@btst/stack 1.5.0 → 1.5.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 (53) hide show
  1. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +7 -6
  2. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +7 -6
  3. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/home-page.internal.cjs +9 -7
  4. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/home-page.internal.mjs +9 -7
  5. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +7 -6
  6. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +7 -6
  7. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +7 -5
  8. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +7 -5
  9. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/tag-page.internal.cjs +5 -3
  10. package/dist/packages/better-stack/src/plugins/blog/client/components/pages/tag-page.internal.mjs +5 -3
  11. package/dist/packages/better-stack/src/plugins/blog/client/plugin.cjs +6 -6
  12. package/dist/packages/better-stack/src/plugins/blog/client/plugin.mjs +6 -6
  13. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +19 -2
  14. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +19 -2
  15. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-list-page.internal.cjs +19 -6
  16. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-list-page.internal.mjs +19 -6
  17. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/dashboard-page.internal.cjs +18 -2
  18. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/dashboard-page.internal.mjs +18 -2
  19. package/dist/packages/better-stack/src/plugins/cms/client/plugin.cjs +112 -22
  20. package/dist/packages/better-stack/src/plugins/cms/client/plugin.mjs +112 -22
  21. package/dist/packages/{better-stack/src/plugins/blog/client/components/shared → ui/src/hooks}/use-route-lifecycle.cjs +8 -9
  22. package/dist/packages/{better-stack/src/plugins/blog/client/components/shared → ui/src/hooks}/use-route-lifecycle.mjs +1 -2
  23. package/dist/plugins/blog/api/index.d.cts +1 -1
  24. package/dist/plugins/blog/api/index.d.mts +1 -1
  25. package/dist/plugins/blog/api/index.d.ts +1 -1
  26. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  27. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  28. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  29. package/dist/plugins/blog/client/index.d.cts +1 -1
  30. package/dist/plugins/blog/client/index.d.mts +1 -1
  31. package/dist/plugins/blog/client/index.d.ts +1 -1
  32. package/dist/plugins/blog/query-keys.d.cts +2 -2
  33. package/dist/plugins/blog/query-keys.d.mts +2 -2
  34. package/dist/plugins/blog/query-keys.d.ts +2 -2
  35. package/dist/plugins/cms/client/index.d.cts +66 -1
  36. package/dist/plugins/cms/client/index.d.mts +66 -1
  37. package/dist/plugins/cms/client/index.d.ts +66 -1
  38. package/package.json +1 -1
  39. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +4 -3
  40. package/src/plugins/blog/client/components/pages/home-page.internal.tsx +4 -2
  41. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +4 -3
  42. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -2
  43. package/src/plugins/blog/client/components/pages/tag-page.internal.tsx +4 -2
  44. package/src/plugins/blog/client/plugin.tsx +10 -9
  45. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +21 -3
  46. package/src/plugins/cms/client/components/pages/content-list-page.internal.tsx +21 -6
  47. package/src/plugins/cms/client/components/pages/dashboard-page.internal.tsx +20 -3
  48. package/src/plugins/cms/client/index.ts +1 -1
  49. package/src/plugins/cms/client/plugin.tsx +236 -25
  50. package/src/plugins/blog/client/components/shared/use-route-lifecycle.tsx +0 -68
  51. package/dist/shared/{stack.CcI4sYJP.d.ts → stack.DLhzx1-D.d.cts} +1 -1
  52. package/dist/shared/{stack.CcI4sYJP.d.cts → stack.DLhzx1-D.d.mts} +1 -1
  53. package/dist/shared/{stack.CcI4sYJP.d.mts → stack.DLhzx1-D.d.ts} +1 -1
@@ -29,24 +29,104 @@ const ContentEditorPageComponent = lazy(() =>
29
29
  * Context passed to loader hooks
30
30
  */
31
31
  export interface LoaderContext {
32
+ /** Current route path */
32
33
  path: string;
34
+ /** Route parameters (e.g., { typeSlug: "product", id: "123" }) */
33
35
  params?: Record<string, string>;
36
+ /** Whether rendering on server (true) or client (false) */
34
37
  isSSR: boolean;
38
+ /** Base URL for API calls */
35
39
  apiBaseURL: string;
40
+ /** Path where the API is mounted */
36
41
  apiBasePath: string;
42
+ /** Optional headers for the request */
37
43
  headers?: Headers;
44
+ /** Additional context properties */
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ /**
49
+ * Hooks for CMS client plugin
50
+ * All hooks are optional and allow consumers to customize behavior
51
+ */
52
+ export interface CMSClientHooks {
53
+ /**
54
+ * Called before loading the dashboard page. Return false to cancel loading.
55
+ * @param context - Loader context with path, params, etc.
56
+ */
57
+ beforeLoadDashboard?: (context: LoaderContext) => Promise<boolean> | boolean;
58
+ /**
59
+ * Called after the dashboard is loaded.
60
+ * @param context - Loader context
61
+ */
62
+ afterLoadDashboard?: (context: LoaderContext) => Promise<void> | void;
63
+ /**
64
+ * Called before loading a content list page. Return false to cancel loading.
65
+ * @param typeSlug - The content type slug
66
+ * @param context - Loader context
67
+ */
68
+ beforeLoadContentList?: (
69
+ typeSlug: string,
70
+ context: LoaderContext,
71
+ ) => Promise<boolean> | boolean;
72
+ /**
73
+ * Called after a content list is loaded.
74
+ * @param typeSlug - The content type slug
75
+ * @param context - Loader context
76
+ */
77
+ afterLoadContentList?: (
78
+ typeSlug: string,
79
+ context: LoaderContext,
80
+ ) => Promise<void> | void;
81
+ /**
82
+ * Called before loading the content editor page. Return false to cancel loading.
83
+ * @param typeSlug - The content type slug
84
+ * @param id - The content item ID (undefined for new items)
85
+ * @param context - Loader context
86
+ */
87
+ beforeLoadContentEditor?: (
88
+ typeSlug: string,
89
+ id: string | undefined,
90
+ context: LoaderContext,
91
+ ) => Promise<boolean> | boolean;
92
+ /**
93
+ * Called after the content editor is loaded.
94
+ * @param typeSlug - The content type slug
95
+ * @param id - The content item ID (undefined for new items)
96
+ * @param context - Loader context
97
+ */
98
+ afterLoadContentEditor?: (
99
+ typeSlug: string,
100
+ id: string | undefined,
101
+ context: LoaderContext,
102
+ ) => Promise<void> | void;
103
+ /**
104
+ * Called when a loading error occurs.
105
+ * Use this for redirects on authorization failures.
106
+ * @param error - The error that occurred
107
+ * @param context - Loader context
108
+ */
109
+ onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
38
110
  }
39
111
 
40
112
  /**
41
113
  * Configuration for CMS client plugin
42
114
  */
43
115
  export interface CMSClientConfig {
116
+ /** Base URL for API calls (e.g., "http://localhost:3000") */
44
117
  apiBaseURL: string;
118
+ /** Path where the API is mounted (e.g., "/api/data") */
45
119
  apiBasePath: string;
120
+ /** Base URL of your site */
46
121
  siteBaseURL: string;
122
+ /** Path where pages are mounted (e.g., "/pages") */
47
123
  siteBasePath: string;
124
+ /** React Query client instance for caching */
48
125
  queryClient: QueryClient;
126
+ /** Optional headers for SSR (e.g., forwarding cookies) */
49
127
  headers?: Headers;
128
+ /** Optional hooks for customizing behavior (authorization, redirects, etc.) */
129
+ hooks?: CMSClientHooks;
50
130
  }
51
131
 
52
132
  /**
@@ -55,17 +135,56 @@ export interface CMSClientConfig {
55
135
  function createDashboardLoader(config: CMSClientConfig) {
56
136
  return async () => {
57
137
  if (typeof window === "undefined") {
58
- const { queryClient, apiBasePath, apiBaseURL, headers } = config;
59
- const client = createApiClient<CMSApiRouter>({
60
- baseURL: apiBaseURL,
61
- basePath: apiBasePath,
62
- });
63
- const queries = createCMSQueryKeys(client, headers);
138
+ const { queryClient, apiBasePath, apiBaseURL, headers, hooks } = config;
139
+
140
+ const context: LoaderContext = {
141
+ path: "/cms",
142
+ isSSR: true,
143
+ apiBaseURL,
144
+ apiBasePath,
145
+ headers,
146
+ };
64
147
 
65
148
  try {
149
+ // Before hook - authorization check
150
+ if (hooks?.beforeLoadDashboard) {
151
+ const canLoad = await hooks.beforeLoadDashboard(context);
152
+ if (!canLoad) {
153
+ throw new Error("Load prevented by beforeLoadDashboard hook");
154
+ }
155
+ }
156
+
157
+ const client = createApiClient<CMSApiRouter>({
158
+ baseURL: apiBaseURL,
159
+ basePath: apiBasePath,
160
+ });
161
+ const queries = createCMSQueryKeys(client, headers);
162
+
66
163
  await queryClient.prefetchQuery(queries.cmsTypes.list());
67
- } catch {
68
- // Let Error Boundaries handle errors during render
164
+
165
+ // After hook
166
+ if (hooks?.afterLoadDashboard) {
167
+ await hooks.afterLoadDashboard(context);
168
+ }
169
+
170
+ // Check if there was an error
171
+ const queryState = queryClient.getQueryState(
172
+ queries.cmsTypes.list().queryKey,
173
+ );
174
+ if (queryState?.error && hooks?.onLoadError) {
175
+ const error =
176
+ queryState.error instanceof Error
177
+ ? queryState.error
178
+ : new Error(String(queryState.error));
179
+ await hooks.onLoadError(error, context);
180
+ }
181
+ } catch (error) {
182
+ // Error hook - log the error but don't throw during SSR
183
+ // Let Error Boundaries handle errors when components render
184
+ if (hooks?.onLoadError) {
185
+ await hooks.onLoadError(error as Error, context);
186
+ }
187
+ // Don't re-throw - let Error Boundary catch it during render
69
188
  }
70
189
  }
71
190
  };
@@ -77,15 +196,33 @@ function createDashboardLoader(config: CMSClientConfig) {
77
196
  function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
78
197
  return async () => {
79
198
  if (typeof window === "undefined") {
80
- const { queryClient, apiBasePath, apiBaseURL, headers } = config;
81
- const client = createApiClient<CMSApiRouter>({
82
- baseURL: apiBaseURL,
83
- basePath: apiBasePath,
84
- });
85
- const queries = createCMSQueryKeys(client, headers);
86
- const limit = 20;
199
+ const { queryClient, apiBasePath, apiBaseURL, headers, hooks } = config;
200
+
201
+ const context: LoaderContext = {
202
+ path: `/cms/${typeSlug}`,
203
+ params: { typeSlug },
204
+ isSSR: true,
205
+ apiBaseURL,
206
+ apiBasePath,
207
+ headers,
208
+ };
87
209
 
88
210
  try {
211
+ // Before hook - authorization check
212
+ if (hooks?.beforeLoadContentList) {
213
+ const canLoad = await hooks.beforeLoadContentList(typeSlug, context);
214
+ if (!canLoad) {
215
+ throw new Error("Load prevented by beforeLoadContentList hook");
216
+ }
217
+ }
218
+
219
+ const client = createApiClient<CMSApiRouter>({
220
+ baseURL: apiBaseURL,
221
+ basePath: apiBasePath,
222
+ });
223
+ const queries = createCMSQueryKeys(client, headers);
224
+ const limit = 20;
225
+
89
226
  // Prefetch content types
90
227
  await queryClient.prefetchQuery(queries.cmsTypes.list());
91
228
 
@@ -116,8 +253,32 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
116
253
  },
117
254
  initialPageParam: 0,
118
255
  });
119
- } catch {
120
- // Let Error Boundaries handle errors during render
256
+
257
+ // After hook
258
+ if (hooks?.afterLoadContentList) {
259
+ await hooks.afterLoadContentList(typeSlug, context);
260
+ }
261
+
262
+ // Check if there was an error in either query
263
+ const typesState = queryClient.getQueryState(
264
+ queries.cmsTypes.list().queryKey,
265
+ );
266
+ const listState = queryClient.getQueryState(listQuery.queryKey);
267
+ const queryError = typesState?.error || listState?.error;
268
+ if (queryError && hooks?.onLoadError) {
269
+ const error =
270
+ queryError instanceof Error
271
+ ? queryError
272
+ : new Error(String(queryError));
273
+ await hooks.onLoadError(error, context);
274
+ }
275
+ } catch (error) {
276
+ // Error hook - log the error but don't throw during SSR
277
+ // Let Error Boundaries handle errors when components render
278
+ if (hooks?.onLoadError) {
279
+ await hooks.onLoadError(error as Error, context);
280
+ }
281
+ // Don't re-throw - let Error Boundary catch it during render
121
282
  }
122
283
  }
123
284
  };
@@ -133,14 +294,36 @@ function createContentEditorLoader(
133
294
  ) {
134
295
  return async () => {
135
296
  if (typeof window === "undefined") {
136
- const { queryClient, apiBasePath, apiBaseURL, headers } = config;
137
- const client = createApiClient<CMSApiRouter>({
138
- baseURL: apiBaseURL,
139
- basePath: apiBasePath,
140
- });
141
- const queries = createCMSQueryKeys(client, headers);
297
+ const { queryClient, apiBasePath, apiBaseURL, headers, hooks } = config;
298
+
299
+ const context: LoaderContext = {
300
+ path: id ? `/cms/${typeSlug}/${id}` : `/cms/${typeSlug}/new`,
301
+ params: id ? { typeSlug, id } : { typeSlug },
302
+ isSSR: true,
303
+ apiBaseURL,
304
+ apiBasePath,
305
+ headers,
306
+ };
142
307
 
143
308
  try {
309
+ // Before hook - authorization check
310
+ if (hooks?.beforeLoadContentEditor) {
311
+ const canLoad = await hooks.beforeLoadContentEditor(
312
+ typeSlug,
313
+ id,
314
+ context,
315
+ );
316
+ if (!canLoad) {
317
+ throw new Error("Load prevented by beforeLoadContentEditor hook");
318
+ }
319
+ }
320
+
321
+ const client = createApiClient<CMSApiRouter>({
322
+ baseURL: apiBaseURL,
323
+ basePath: apiBasePath,
324
+ });
325
+ const queries = createCMSQueryKeys(client, headers);
326
+
144
327
  const promises = [queryClient.prefetchQuery(queries.cmsTypes.list())];
145
328
  if (id) {
146
329
  promises.push(
@@ -148,8 +331,36 @@ function createContentEditorLoader(
148
331
  );
149
332
  }
150
333
  await Promise.all(promises);
151
- } catch {
152
- // Let Error Boundaries handle errors during render
334
+
335
+ // After hook
336
+ if (hooks?.afterLoadContentEditor) {
337
+ await hooks.afterLoadContentEditor(typeSlug, id, context);
338
+ }
339
+
340
+ // Check if there was an error
341
+ const typesState = queryClient.getQueryState(
342
+ queries.cmsTypes.list().queryKey,
343
+ );
344
+ const itemState = id
345
+ ? queryClient.getQueryState(
346
+ queries.cmsContent.detail(typeSlug, id).queryKey,
347
+ )
348
+ : null;
349
+ const queryError = typesState?.error || itemState?.error;
350
+ if (queryError && hooks?.onLoadError) {
351
+ const error =
352
+ queryError instanceof Error
353
+ ? queryError
354
+ : new Error(String(queryError));
355
+ await hooks.onLoadError(error, context);
356
+ }
357
+ } catch (error) {
358
+ // Error hook - log the error but don't throw during SSR
359
+ // Let Error Boundaries handle errors when components render
360
+ if (hooks?.onLoadError) {
361
+ await hooks.onLoadError(error as Error, context);
362
+ }
363
+ // Don't re-throw - let Error Boundary catch it during render
153
364
  }
154
365
  }
155
366
  };
@@ -1,68 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect } from "react";
4
- import { usePluginOverrides } from "@btst/stack/context";
5
- import type { BlogPluginOverrides, RouteContext } from "../../overrides";
6
-
7
- /**
8
- * Hook to handle route lifecycle events
9
- * - Calls authorization check before render
10
- * - Calls onRouteRender on mount
11
- * - Handles errors with onRouteError
12
- */
13
- export function useRouteLifecycle({
14
- routeName,
15
- context,
16
- beforeRenderHook,
17
- }: {
18
- routeName: string;
19
- context: RouteContext;
20
- beforeRenderHook?: (
21
- overrides: BlogPluginOverrides,
22
- context: RouteContext,
23
- ) => boolean;
24
- }) {
25
- const overrides = usePluginOverrides<BlogPluginOverrides>("blog");
26
-
27
- // Authorization check - runs synchronously before render
28
- if (beforeRenderHook) {
29
- const canRender = beforeRenderHook(overrides, context);
30
- if (!canRender) {
31
- const error = new Error(`Unauthorized: Cannot render ${routeName}`);
32
- // Call error hook synchronously
33
- if (overrides.onRouteError) {
34
- try {
35
- const result = overrides.onRouteError(routeName, error, context);
36
- if (result instanceof Promise) {
37
- result.catch(() => {}); // Ignore promise rejection
38
- }
39
- } catch {
40
- // Ignore errors in error hook
41
- }
42
- }
43
- throw error;
44
- }
45
- }
46
-
47
- // Lifecycle hook - runs on mount
48
- useEffect(() => {
49
- if (overrides.onRouteRender) {
50
- try {
51
- const result = overrides.onRouteRender(routeName, context);
52
- if (result instanceof Promise) {
53
- result.catch((error) => {
54
- // If onRouteRender throws, call onRouteError
55
- if (overrides.onRouteError) {
56
- overrides.onRouteError(routeName, error, context);
57
- }
58
- });
59
- }
60
- } catch (error) {
61
- // If onRouteRender throws, call onRouteError
62
- if (overrides.onRouteError) {
63
- overrides.onRouteError(routeName, error as Error, context);
64
- }
65
- }
66
- }
67
- }, [routeName, overrides, context]);
68
- }
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
+ title: z.ZodString;
38
39
  slug: z.ZodOptional<z.ZodString>;
39
40
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
40
41
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
41
- title: z.ZodString;
42
42
  content: z.ZodString;
43
43
  excerpt: z.ZodString;
44
44
  image: z.ZodOptional<z.ZodString>;
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
+ title: z.ZodString;
38
39
  slug: z.ZodOptional<z.ZodString>;
39
40
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
40
41
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
41
- title: z.ZodString;
42
42
  content: z.ZodString;
43
43
  excerpt: z.ZodString;
44
44
  image: z.ZodOptional<z.ZodString>;
@@ -35,10 +35,10 @@ interface SerializedTag extends Omit<Tag, "createdAt" | "updatedAt"> {
35
35
  }
36
36
 
37
37
  declare const createPostSchema: z.ZodObject<{
38
+ title: z.ZodString;
38
39
  slug: z.ZodOptional<z.ZodString>;
39
40
  published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
40
41
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
41
- title: z.ZodString;
42
42
  content: z.ZodString;
43
43
  excerpt: z.ZodString;
44
44
  image: z.ZodOptional<z.ZodString>;