@btst/stack 2.9.3 → 2.9.4

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 (38) hide show
  1. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +22 -11
  2. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +22 -11
  3. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +71 -39
  4. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +71 -39
  5. package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +62 -2
  6. package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +63 -3
  7. package/dist/packages/stack/src/plugins/comments/client/utils.cjs +2 -11
  8. package/dist/packages/stack/src/plugins/comments/client/utils.mjs +2 -11
  9. package/dist/packages/stack/src/plugins/comments/error-utils.cjs +15 -0
  10. package/dist/packages/stack/src/plugins/comments/error-utils.mjs +13 -0
  11. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +59 -31
  12. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +59 -31
  13. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +52 -25
  14. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +53 -26
  15. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  16. package/dist/packages/stack/src/plugins/utils.mjs +5 -1
  17. package/dist/plugins/client/index.cjs +2 -0
  18. package/dist/plugins/client/index.d.cts +15 -1
  19. package/dist/plugins/client/index.d.mts +15 -1
  20. package/dist/plugins/client/index.d.ts +15 -1
  21. package/dist/plugins/client/index.mjs +1 -1
  22. package/dist/plugins/comments/client/index.d.cts +5 -0
  23. package/dist/plugins/comments/client/index.d.mts +5 -0
  24. package/dist/plugins/comments/client/index.d.ts +5 -0
  25. package/dist/plugins/comments/query-keys.cjs +4 -4
  26. package/dist/plugins/comments/query-keys.mjs +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/client-plugin-ssr-loaders.test.ts +329 -0
  29. package/src/plugins/blog/client/plugin.tsx +23 -14
  30. package/src/plugins/client/index.ts +2 -0
  31. package/src/plugins/cms/client/plugin.tsx +73 -42
  32. package/src/plugins/comments/client/plugin.tsx +82 -2
  33. package/src/plugins/comments/client/utils.ts +2 -14
  34. package/src/plugins/comments/error-utils.ts +17 -0
  35. package/src/plugins/comments/query-keys.ts +1 -1
  36. package/src/plugins/form-builder/client/plugin.tsx +59 -35
  37. package/src/plugins/ui-builder/client/plugin.tsx +57 -27
  38. package/src/plugins/utils.ts +18 -0
@@ -1,8 +1,9 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { lazy } from 'react';
3
- import { defineClientPlugin, createApiClient, runClientHookWithShim } from '@btst/stack/plugins/client';
3
+ import { defineClientPlugin, createApiClient, runClientHookWithShim, isConnectionError } from '@btst/stack/plugins/client';
4
4
  import { createRoute } from '@btst/yar';
5
5
  import { createCMSQueryKeys } from '../../../../../../plugins/cms/query-keys.mjs';
6
+ import { createSanitizedSSRLoaderError } from '../../utils.mjs';
6
7
  import { UI_BUILDER_TYPE_SLUG } from '../schemas.mjs';
7
8
 
8
9
  const PageListPageComponent = lazy(
@@ -27,6 +28,18 @@ function createPageListLoader(config) {
27
28
  apiBasePath,
28
29
  headers
29
30
  };
31
+ const client = createApiClient({
32
+ baseURL: apiBaseURL,
33
+ basePath: apiBasePath
34
+ });
35
+ const queries = createCMSQueryKeys(client, headers);
36
+ const limit = 20;
37
+ const listQuery = queries.cmsContent.list({
38
+ typeSlug,
39
+ limit,
40
+ offset: 0
41
+ });
42
+ const uiBuilderListQueryKey = [...listQuery.queryKey, "ui-builder"];
30
43
  try {
31
44
  if (hooks?.beforeLoadPageList) {
32
45
  await runClientHookWithShim(
@@ -34,19 +47,8 @@ function createPageListLoader(config) {
34
47
  "Load prevented by beforeLoadPageList hook"
35
48
  );
36
49
  }
37
- const client = createApiClient({
38
- baseURL: apiBaseURL,
39
- basePath: apiBasePath
40
- });
41
- const queries = createCMSQueryKeys(client, headers);
42
- const limit = 20;
43
- const listQuery = queries.cmsContent.list({
44
- typeSlug,
45
- limit,
46
- offset: 0
47
- });
48
50
  await queryClient.prefetchInfiniteQuery({
49
- queryKey: [...listQuery.queryKey, "ui-builder"],
51
+ queryKey: uiBuilderListQueryKey,
50
52
  queryFn: async ({ pageParam = 0 }) => {
51
53
  const response = await client("/content/:typeSlug", {
52
54
  method: "GET",
@@ -65,14 +67,28 @@ function createPageListLoader(config) {
65
67
  await hooks.afterLoadPageList(context);
66
68
  }
67
69
  const queryState = queryClient.getQueryState([
68
- ...listQuery.queryKey,
69
- "ui-builder"
70
+ ...uiBuilderListQueryKey
70
71
  ]);
71
72
  if (queryState?.error && hooks?.onLoadError) {
72
73
  const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
73
74
  await hooks.onLoadError(error, context);
74
75
  }
75
76
  } catch (error) {
77
+ if (isConnectionError(error)) {
78
+ console.warn(
79
+ "[btst/ui-builder] route.loader() failed \u2014 no server running at build time. Use myStack.api.uiBuilder.prefetchForRoute() for SSG data prefetching."
80
+ );
81
+ } else {
82
+ const errToStore = createSanitizedSSRLoaderError();
83
+ await queryClient.prefetchInfiniteQuery({
84
+ queryKey: uiBuilderListQueryKey,
85
+ queryFn: () => {
86
+ throw errToStore;
87
+ },
88
+ initialPageParam: 0,
89
+ retry: false
90
+ });
91
+ }
76
92
  if (hooks?.onLoadError) {
77
93
  await hooks.onLoadError(error, context);
78
94
  }
@@ -93,6 +109,12 @@ function createPageBuilderLoader(id, config) {
93
109
  apiBasePath,
94
110
  headers
95
111
  };
112
+ const client = createApiClient({
113
+ baseURL: apiBaseURL,
114
+ basePath: apiBasePath
115
+ });
116
+ const queries = createCMSQueryKeys(client, headers);
117
+ const pageQuery = id ? queries.cmsContent.detail(typeSlug, id) : void 0;
96
118
  try {
97
119
  if (hooks?.beforeLoadPageBuilder) {
98
120
  await runClientHookWithShim(
@@ -100,29 +122,34 @@ function createPageBuilderLoader(id, config) {
100
122
  "Load prevented by beforeLoadPageBuilder hook"
101
123
  );
102
124
  }
103
- const client = createApiClient({
104
- baseURL: apiBaseURL,
105
- basePath: apiBasePath
106
- });
107
- const queries = createCMSQueryKeys(client, headers);
108
125
  if (id) {
109
- await queryClient.prefetchQuery(
110
- queries.cmsContent.detail(typeSlug, id)
111
- );
126
+ await queryClient.prefetchQuery(pageQuery);
112
127
  }
113
128
  if (hooks?.afterLoadPageBuilder) {
114
129
  await hooks.afterLoadPageBuilder(id, context);
115
130
  }
116
131
  if (id) {
117
- const queryState = queryClient.getQueryState(
118
- queries.cmsContent.detail(typeSlug, id).queryKey
119
- );
132
+ const queryState = queryClient.getQueryState(pageQuery.queryKey);
120
133
  if (queryState?.error && hooks?.onLoadError) {
121
134
  const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
122
135
  await hooks.onLoadError(error, context);
123
136
  }
124
137
  }
125
138
  } catch (error) {
139
+ if (isConnectionError(error)) {
140
+ console.warn(
141
+ "[btst/ui-builder] route.loader() failed \u2014 no server running at build time. Use myStack.api.uiBuilder.prefetchForRoute() for SSG data prefetching."
142
+ );
143
+ } else if (pageQuery) {
144
+ const errToStore = createSanitizedSSRLoaderError();
145
+ await queryClient.prefetchQuery({
146
+ queryKey: pageQuery.queryKey,
147
+ queryFn: () => {
148
+ throw errToStore;
149
+ },
150
+ retry: false
151
+ });
152
+ }
126
153
  if (hooks?.onLoadError) {
127
154
  await hooks.onLoadError(error, context);
128
155
  }
@@ -47,6 +47,10 @@ function isConnectionError(err) {
47
47
  const code = err.cause?.code ?? err.code;
48
48
  return err.message.includes("ECONNREFUSED") || err.message.includes("fetch failed") || err.message.includes("ERR_CONNECTION_REFUSED") || code === "ECONNREFUSED" || code === "ERR_CONNECTION_REFUSED";
49
49
  }
50
+ const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
51
+ function createSanitizedSSRLoaderError() {
52
+ return new Error(SSR_LOADER_ERROR_MESSAGE);
53
+ }
50
54
  function createApiClient(options) {
51
55
  const { baseURL = "", basePath = "/" } = options ?? {};
52
56
  const normalizedBaseURL = baseURL ? baseURL.replace(/\/$/, "") : "";
@@ -58,7 +62,9 @@ function createApiClient(options) {
58
62
  });
59
63
  }
60
64
 
65
+ exports.SSR_LOADER_ERROR_MESSAGE = SSR_LOADER_ERROR_MESSAGE;
61
66
  exports.createApiClient = createApiClient;
67
+ exports.createSanitizedSSRLoaderError = createSanitizedSSRLoaderError;
62
68
  exports.isConnectionError = isConnectionError;
63
69
  exports.runClientHookWithShim = runClientHookWithShim;
64
70
  exports.runHookWithShim = runHookWithShim;
@@ -45,6 +45,10 @@ function isConnectionError(err) {
45
45
  const code = err.cause?.code ?? err.code;
46
46
  return err.message.includes("ECONNREFUSED") || err.message.includes("fetch failed") || err.message.includes("ERR_CONNECTION_REFUSED") || code === "ECONNREFUSED" || code === "ERR_CONNECTION_REFUSED";
47
47
  }
48
+ const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
49
+ function createSanitizedSSRLoaderError() {
50
+ return new Error(SSR_LOADER_ERROR_MESSAGE);
51
+ }
48
52
  function createApiClient(options) {
49
53
  const { baseURL = "", basePath = "/" } = options ?? {};
50
54
  const normalizedBaseURL = baseURL ? baseURL.replace(/\/$/, "") : "";
@@ -56,4 +60,4 @@ function createApiClient(options) {
56
60
  });
57
61
  }
58
62
 
59
- export { createApiClient, isConnectionError, runClientHookWithShim, runHookWithShim };
63
+ export { SSR_LOADER_ERROR_MESSAGE, createApiClient, createSanitizedSSRLoaderError, isConnectionError, runClientHookWithShim, runHookWithShim };
@@ -8,7 +8,9 @@ function defineClientPlugin(plugin) {
8
8
  return plugin;
9
9
  }
10
10
 
11
+ exports.SSR_LOADER_ERROR_MESSAGE = utils.SSR_LOADER_ERROR_MESSAGE;
11
12
  exports.createApiClient = utils.createApiClient;
13
+ exports.createSanitizedSSRLoaderError = utils.createSanitizedSSRLoaderError;
12
14
  exports.isConnectionError = utils.isConnectionError;
13
15
  exports.runClientHookWithShim = utils.runClientHookWithShim;
14
16
  exports.createRoute = yar.createRoute;
@@ -20,6 +20,20 @@ declare function runClientHookWithShim<T>(hookFn: () => Promise<T> | T, defaultM
20
20
  * `route.loader()` is called during `next build` with no HTTP server running.
21
21
  */
22
22
  declare function isConnectionError(err: unknown): boolean;
23
+ /**
24
+ * Public-safe message used when SSR loader failures are intentionally seeded
25
+ * into React Query so Error Boundaries can render on the client.
26
+ *
27
+ * Never include raw server error text here because dehydrated query state can
28
+ * be serialized into HTML.
29
+ */
30
+ declare const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
31
+ /**
32
+ * Creates a sanitized Error for SSR loader cache seeding.
33
+ *
34
+ * Use this instead of storing raw server errors in dehydrated query state.
35
+ */
36
+ declare function createSanitizedSSRLoaderError(): Error;
23
37
 
24
38
  interface CreateApiClientOptions {
25
39
  baseURL?: string;
@@ -67,4 +81,4 @@ declare function createApiClient<TRouter extends Router | Record<string, Endpoin
67
81
  */
68
82
  declare function defineClientPlugin<TOverrides = Record<string, never>, TRoutes extends Record<string, Route> = Record<string, Route>>(plugin: ClientPlugin<TOverrides, TRoutes>): ClientPlugin<TOverrides, TRoutes>;
69
83
 
70
- export { ClientPlugin, createApiClient, defineClientPlugin, isConnectionError, runClientHookWithShim };
84
+ export { ClientPlugin, SSR_LOADER_ERROR_MESSAGE, createApiClient, createSanitizedSSRLoaderError, defineClientPlugin, isConnectionError, runClientHookWithShim };
@@ -20,6 +20,20 @@ declare function runClientHookWithShim<T>(hookFn: () => Promise<T> | T, defaultM
20
20
  * `route.loader()` is called during `next build` with no HTTP server running.
21
21
  */
22
22
  declare function isConnectionError(err: unknown): boolean;
23
+ /**
24
+ * Public-safe message used when SSR loader failures are intentionally seeded
25
+ * into React Query so Error Boundaries can render on the client.
26
+ *
27
+ * Never include raw server error text here because dehydrated query state can
28
+ * be serialized into HTML.
29
+ */
30
+ declare const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
31
+ /**
32
+ * Creates a sanitized Error for SSR loader cache seeding.
33
+ *
34
+ * Use this instead of storing raw server errors in dehydrated query state.
35
+ */
36
+ declare function createSanitizedSSRLoaderError(): Error;
23
37
 
24
38
  interface CreateApiClientOptions {
25
39
  baseURL?: string;
@@ -67,4 +81,4 @@ declare function createApiClient<TRouter extends Router | Record<string, Endpoin
67
81
  */
68
82
  declare function defineClientPlugin<TOverrides = Record<string, never>, TRoutes extends Record<string, Route> = Record<string, Route>>(plugin: ClientPlugin<TOverrides, TRoutes>): ClientPlugin<TOverrides, TRoutes>;
69
83
 
70
- export { ClientPlugin, createApiClient, defineClientPlugin, isConnectionError, runClientHookWithShim };
84
+ export { ClientPlugin, SSR_LOADER_ERROR_MESSAGE, createApiClient, createSanitizedSSRLoaderError, defineClientPlugin, isConnectionError, runClientHookWithShim };
@@ -20,6 +20,20 @@ declare function runClientHookWithShim<T>(hookFn: () => Promise<T> | T, defaultM
20
20
  * `route.loader()` is called during `next build` with no HTTP server running.
21
21
  */
22
22
  declare function isConnectionError(err: unknown): boolean;
23
+ /**
24
+ * Public-safe message used when SSR loader failures are intentionally seeded
25
+ * into React Query so Error Boundaries can render on the client.
26
+ *
27
+ * Never include raw server error text here because dehydrated query state can
28
+ * be serialized into HTML.
29
+ */
30
+ declare const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
31
+ /**
32
+ * Creates a sanitized Error for SSR loader cache seeding.
33
+ *
34
+ * Use this instead of storing raw server errors in dehydrated query state.
35
+ */
36
+ declare function createSanitizedSSRLoaderError(): Error;
23
37
 
24
38
  interface CreateApiClientOptions {
25
39
  baseURL?: string;
@@ -67,4 +81,4 @@ declare function createApiClient<TRouter extends Router | Record<string, Endpoin
67
81
  */
68
82
  declare function defineClientPlugin<TOverrides = Record<string, never>, TRoutes extends Record<string, Route> = Record<string, Route>>(plugin: ClientPlugin<TOverrides, TRoutes>): ClientPlugin<TOverrides, TRoutes>;
69
83
 
70
- export { ClientPlugin, createApiClient, defineClientPlugin, isConnectionError, runClientHookWithShim };
84
+ export { ClientPlugin, SSR_LOADER_ERROR_MESSAGE, createApiClient, createSanitizedSSRLoaderError, defineClientPlugin, isConnectionError, runClientHookWithShim };
@@ -1,4 +1,4 @@
1
- export { createApiClient, isConnectionError, runClientHookWithShim } from '../../packages/stack/src/plugins/utils.mjs';
1
+ export { SSR_LOADER_ERROR_MESSAGE, createApiClient, createSanitizedSSRLoaderError, isConnectionError, runClientHookWithShim } from '../../packages/stack/src/plugins/utils.mjs';
2
2
  export { createRoute, createRouter } from '@btst/yar';
3
3
  export { createClient } from 'better-call/client';
4
4
 
@@ -21,6 +21,11 @@ interface LoaderContext {
21
21
  apiBasePath: string;
22
22
  /** Optional headers for the request */
23
23
  headers?: Headers;
24
+ /**
25
+ * Optional current user ID for SSR loaders that need user-scoped query keys.
26
+ * Hooks (e.g. beforeLoadUserComments) may populate this.
27
+ */
28
+ currentUserId?: string;
24
29
  /** Additional context properties */
25
30
  [key: string]: unknown;
26
31
  }
@@ -21,6 +21,11 @@ interface LoaderContext {
21
21
  apiBasePath: string;
22
22
  /** Optional headers for the request */
23
23
  headers?: Headers;
24
+ /**
25
+ * Optional current user ID for SSR loaders that need user-scoped query keys.
26
+ * Hooks (e.g. beforeLoadUserComments) may populate this.
27
+ */
28
+ currentUserId?: string;
24
29
  /** Additional context properties */
25
30
  [key: string]: unknown;
26
31
  }
@@ -21,6 +21,11 @@ interface LoaderContext {
21
21
  apiBasePath: string;
22
22
  /** Optional headers for the request */
23
23
  headers?: Headers;
24
+ /**
25
+ * Optional current user ID for SSR loaders that need user-scoped query keys.
26
+ * Hooks (e.g. beforeLoadUserComments) may populate this.
27
+ */
28
+ currentUserId?: string;
24
29
  /** Additional context properties */
25
30
  [key: string]: unknown;
26
31
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const queryKeyFactory = require('@lukemorales/query-key-factory');
4
4
  const queryKeyDefs = require('../../packages/stack/src/plugins/comments/api/query-key-defs.cjs');
5
- const utils = require('../../packages/stack/src/plugins/comments/client/utils.cjs');
5
+ const errorUtils = require('../../packages/stack/src/plugins/comments/error-utils.cjs');
6
6
 
7
7
  function isErrorResponse(response) {
8
8
  return typeof response === "object" && response !== null && "error" in response && response.error !== null && response.error !== void 0;
@@ -40,7 +40,7 @@ function createCommentsQueries(client, headers) {
40
40
  headers
41
41
  });
42
42
  if (isErrorResponse(response)) {
43
- throw utils.toError(response.error);
43
+ throw errorUtils.toError(response.error);
44
44
  }
45
45
  const data = response.data;
46
46
  return data ?? { items: [], total: 0, limit: 20, offset: 0 };
@@ -63,7 +63,7 @@ function createCommentCountQueries(client, headers) {
63
63
  headers
64
64
  });
65
65
  if (isErrorResponse(response)) {
66
- throw utils.toError(response.error);
66
+ throw errorUtils.toError(response.error);
67
67
  }
68
68
  const data = response.data;
69
69
  return data?.count ?? 0;
@@ -96,7 +96,7 @@ function createCommentsThreadQueries(client, headers) {
96
96
  headers
97
97
  });
98
98
  if (isErrorResponse(response)) {
99
- throw utils.toError(response.error);
99
+ throw errorUtils.toError(response.error);
100
100
  }
101
101
  const data = response.data;
102
102
  return data ?? {
@@ -1,6 +1,6 @@
1
1
  import { mergeQueryKeys, createQueryKeys } from '@lukemorales/query-key-factory';
2
2
  import { commentsListDiscriminator, commentCountDiscriminator, commentsThreadDiscriminator } from '../../packages/stack/src/plugins/comments/api/query-key-defs.mjs';
3
- import { toError } from '../../packages/stack/src/plugins/comments/client/utils.mjs';
3
+ import { toError } from '../../packages/stack/src/plugins/comments/error-utils.mjs';
4
4
 
5
5
  function isErrorResponse(response) {
6
6
  return typeof response === "object" && response !== null && "error" in response && response.error !== null && response.error !== void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.9.3",
3
+ "version": "2.9.4",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",