@btst/stack 2.9.3 → 2.10.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.
Files changed (69) hide show
  1. package/README.md +12 -0
  2. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +22 -11
  3. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +22 -11
  4. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +71 -39
  5. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +71 -39
  6. package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +62 -2
  7. package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +63 -3
  8. package/dist/packages/stack/src/plugins/comments/client/utils.cjs +2 -11
  9. package/dist/packages/stack/src/plugins/comments/client/utils.mjs +2 -11
  10. package/dist/packages/stack/src/plugins/comments/error-utils.cjs +15 -0
  11. package/dist/packages/stack/src/plugins/comments/error-utils.mjs +13 -0
  12. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +59 -31
  13. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +59 -31
  14. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +52 -25
  15. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +53 -26
  16. package/dist/packages/stack/src/plugins/utils.cjs +6 -0
  17. package/dist/packages/stack/src/plugins/utils.mjs +5 -1
  18. package/dist/plugins/blog/api/index.d.cts +2 -2
  19. package/dist/plugins/blog/api/index.d.mts +2 -2
  20. package/dist/plugins/blog/api/index.d.ts +2 -2
  21. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  22. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  23. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  24. package/dist/plugins/blog/client/index.d.cts +2 -2
  25. package/dist/plugins/blog/client/index.d.mts +2 -2
  26. package/dist/plugins/blog/client/index.d.ts +2 -2
  27. package/dist/plugins/blog/query-keys.d.cts +2 -2
  28. package/dist/plugins/blog/query-keys.d.mts +2 -2
  29. package/dist/plugins/blog/query-keys.d.ts +2 -2
  30. package/dist/plugins/client/index.cjs +2 -0
  31. package/dist/plugins/client/index.d.cts +15 -1
  32. package/dist/plugins/client/index.d.mts +15 -1
  33. package/dist/plugins/client/index.d.ts +15 -1
  34. package/dist/plugins/client/index.mjs +1 -1
  35. package/dist/plugins/comments/client/index.d.cts +5 -0
  36. package/dist/plugins/comments/client/index.d.mts +5 -0
  37. package/dist/plugins/comments/client/index.d.ts +5 -0
  38. package/dist/plugins/comments/query-keys.cjs +4 -4
  39. package/dist/plugins/comments/query-keys.mjs +1 -1
  40. package/dist/plugins/kanban/api/index.d.cts +1 -1
  41. package/dist/plugins/kanban/api/index.d.mts +1 -1
  42. package/dist/plugins/kanban/api/index.d.ts +1 -1
  43. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  44. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  45. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  46. package/dist/shared/{stack.IUeyQKrm.d.mts → stack.BSqJrCTM.d.cts} +5 -5
  47. package/dist/shared/{stack.D7HSzZdG.d.ts → stack.BXxrFL9R.d.ts} +5 -5
  48. package/dist/shared/{stack.6mEHS2WH.d.mts → stack.DOZ1EXjM.d.mts} +3 -3
  49. package/dist/shared/{stack.AJTXI7kw.d.cts → stack.DX-tQ93o.d.cts} +3 -3
  50. package/dist/shared/{stack.DjgpFWq3.d.cts → stack.DzOhpIYM.d.mts} +5 -5
  51. package/dist/shared/{stack.QYn-Px94.d.ts → stack.VF6FhyZw.d.ts} +3 -3
  52. package/package.json +1 -1
  53. package/src/__tests__/client-plugin-ssr-loaders.test.ts +329 -0
  54. package/src/plugins/blog/client/plugin.tsx +23 -14
  55. package/src/plugins/client/index.ts +2 -0
  56. package/src/plugins/cms/client/plugin.tsx +73 -42
  57. package/src/plugins/comments/client/plugin.tsx +82 -2
  58. package/src/plugins/comments/client/utils.ts +2 -14
  59. package/src/plugins/comments/error-utils.ts +17 -0
  60. package/src/plugins/comments/query-keys.ts +1 -1
  61. package/src/plugins/form-builder/client/plugin.tsx +59 -35
  62. package/src/plugins/ui-builder/client/plugin.tsx +57 -27
  63. package/src/plugins/utils.ts +18 -0
  64. package/dist/shared/{stack.eq5eg1yt.d.cts → stack.BOokfhZD.d.cts} +13 -13
  65. package/dist/shared/{stack.BQmuNl5p.d.ts → stack.BWp0hcm9.d.cts} +3 -3
  66. package/dist/shared/{stack.BQmuNl5p.d.cts → stack.BWp0hcm9.d.mts} +3 -3
  67. package/dist/shared/{stack.BQmuNl5p.d.mts → stack.BWp0hcm9.d.ts} +3 -3
  68. package/dist/shared/{stack.CMbX8Q5C.d.ts → stack.BvCR4-9H.d.ts} +13 -13
  69. package/dist/shared/{stack.Dj04W2c3.d.mts → stack.CWxAl9K3.d.mts} +13 -13
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BQmuNl5p.mjs';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BWp0hcm9.mjs';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /**
@@ -135,14 +135,14 @@ declare function useCreatePost(): _tanstack_react_query.UseMutationResult<Serial
135
135
  name: string;
136
136
  slug: string;
137
137
  })[];
138
+ published: boolean;
138
139
  title: string;
139
140
  content: string;
140
141
  excerpt: string;
141
- published: boolean;
142
142
  slug?: string | undefined;
143
+ publishedAt?: Date | undefined;
143
144
  createdAt?: Date | undefined;
144
145
  updatedAt?: Date | undefined;
145
- publishedAt?: Date | undefined;
146
146
  image?: string | undefined;
147
147
  }, unknown>;
148
148
  /** Update an existing post by id */
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BQmuNl5p.cjs';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BWp0hcm9.cjs';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /**
@@ -135,14 +135,14 @@ declare function useCreatePost(): _tanstack_react_query.UseMutationResult<Serial
135
135
  name: string;
136
136
  slug: string;
137
137
  })[];
138
+ published: boolean;
138
139
  title: string;
139
140
  content: string;
140
141
  excerpt: string;
141
- published: boolean;
142
142
  slug?: string | undefined;
143
+ publishedAt?: Date | undefined;
143
144
  createdAt?: Date | undefined;
144
145
  updatedAt?: Date | undefined;
145
- publishedAt?: Date | undefined;
146
146
  image?: string | undefined;
147
147
  }, unknown>;
148
148
  /** Update an existing post by id */
@@ -1,7 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { QueryClient } from '@tanstack/react-query';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.BQmuNl5p.cjs';
4
+ import { P as Post, T as Tag, c as createPostSchema, u as updatePostSchema, S as SerializedPost, a as SerializedTag } from './stack.BWp0hcm9.mjs';
5
5
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
6
6
  import { DBAdapter } from '@btst/db';
7
7
  import * as better_call from 'better-call';
@@ -244,11 +244,11 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
244
244
  slug: string;
245
245
  })[] | undefined;
246
246
  slug?: string | undefined;
247
+ published?: boolean | undefined;
248
+ publishedAt?: unknown;
247
249
  createdAt?: unknown;
248
250
  updatedAt?: unknown;
249
- publishedAt?: unknown;
250
251
  image?: string | undefined;
251
- published?: boolean | undefined;
252
252
  }, {
253
253
  title: string;
254
254
  content: string;
@@ -261,11 +261,11 @@ declare const blogBackendPlugin: (hooks?: BlogBackendHooks) => _btst_stack_plugi
261
261
  slug: string;
262
262
  })[] | undefined;
263
263
  slug?: string | undefined;
264
+ published?: boolean | undefined;
265
+ publishedAt?: unknown;
264
266
  createdAt?: unknown;
265
267
  updatedAt?: unknown;
266
- publishedAt?: unknown;
267
268
  image?: string | undefined;
268
- published?: boolean | undefined;
269
269
  }>;
270
270
  }, Post>;
271
271
  readonly updatePost: better_call.StrictEndpoint<"/posts/:id", {} & {
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BQmuNl5p.js';
2
+ import { S as SerializedPost, c as createPostSchema, u as updatePostSchema, a as SerializedTag } from './stack.BWp0hcm9.js';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /**
@@ -135,14 +135,14 @@ declare function useCreatePost(): _tanstack_react_query.UseMutationResult<Serial
135
135
  name: string;
136
136
  slug: string;
137
137
  })[];
138
+ published: boolean;
138
139
  title: string;
139
140
  content: string;
140
141
  excerpt: string;
141
- published: boolean;
142
142
  slug?: string | undefined;
143
+ publishedAt?: Date | undefined;
143
144
  createdAt?: Date | undefined;
144
145
  updatedAt?: Date | undefined;
145
- publishedAt?: Date | undefined;
146
146
  image?: string | undefined;
147
147
  }, unknown>;
148
148
  /** Update an existing post by id */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,329 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { QueryClient } from "@tanstack/react-query";
3
+ import { createApiClient, SSR_LOADER_ERROR_MESSAGE } from "../plugins/client";
4
+ import { blogClientPlugin } from "../plugins/blog/client";
5
+ import type { BlogApiRouter } from "../plugins/blog/api";
6
+ import { createBlogQueryKeys } from "../plugins/blog/query-keys";
7
+ import { cmsClientPlugin } from "../plugins/cms/client";
8
+ import type { CMSApiRouter } from "../plugins/cms/api";
9
+ import { createCMSQueryKeys } from "../plugins/cms/query-keys";
10
+ import { formBuilderClientPlugin } from "../plugins/form-builder/client";
11
+ import type { FormBuilderApiRouter } from "../plugins/form-builder/api";
12
+ import { createFormBuilderQueryKeys } from "../plugins/form-builder/query-keys";
13
+ import { uiBuilderClientPlugin } from "../plugins/ui-builder/client";
14
+ import { UI_BUILDER_TYPE_SLUG } from "../plugins/ui-builder";
15
+ import { commentsClientPlugin } from "../plugins/comments/client";
16
+ import type { CommentsApiRouter } from "../plugins/comments/api";
17
+ import { createCommentsQueryKeys } from "../plugins/comments/query-keys";
18
+
19
+ const API_BASE_URL = "http://localhost:3000";
20
+ const API_BASE_PATH = "/api/data";
21
+ const SITE_BASE_URL = "http://localhost:3000";
22
+ const SITE_BASE_PATH = "/pages";
23
+ const TEST_HEADERS = new Headers();
24
+
25
+ function getErrorMessage(
26
+ queryClient: QueryClient,
27
+ queryKey: readonly unknown[],
28
+ ) {
29
+ const error = queryClient.getQueryState(queryKey)?.error;
30
+ return error instanceof Error ? error.message : null;
31
+ }
32
+
33
+ function mockFetchApiError(errorMessage: string) {
34
+ return vi.spyOn(globalThis, "fetch").mockResolvedValue(
35
+ new Response(JSON.stringify({ message: errorMessage }), {
36
+ status: 500,
37
+ headers: { "content-type": "application/json" },
38
+ }),
39
+ );
40
+ }
41
+
42
+ describe("client plugin SSR loaders", () => {
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it("blog drafts loader seeds query error when beforeLoadPosts throws", async () => {
48
+ const queryClient = new QueryClient();
49
+ const expectedError = new Error("blog drafts blocked");
50
+
51
+ const plugin = blogClientPlugin({
52
+ apiBaseURL: API_BASE_URL,
53
+ apiBasePath: API_BASE_PATH,
54
+ siteBaseURL: SITE_BASE_URL,
55
+ siteBasePath: SITE_BASE_PATH,
56
+ queryClient,
57
+ headers: TEST_HEADERS,
58
+ hooks: {
59
+ beforeLoadPosts: () => {
60
+ throw expectedError;
61
+ },
62
+ },
63
+ });
64
+
65
+ const route = plugin.routes().drafts();
66
+ await route.loader?.();
67
+
68
+ const client = createApiClient<BlogApiRouter>({
69
+ baseURL: API_BASE_URL,
70
+ basePath: API_BASE_PATH,
71
+ });
72
+ const queries = createBlogQueryKeys(client, TEST_HEADERS);
73
+ const listQuery = queries.posts.list({
74
+ query: undefined,
75
+ limit: 10,
76
+ published: false,
77
+ });
78
+
79
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
80
+ SSR_LOADER_ERROR_MESSAGE,
81
+ );
82
+ });
83
+
84
+ it("cms content list loader seeds query error when beforeLoadContentList throws", async () => {
85
+ const queryClient = new QueryClient();
86
+ const expectedError = new Error("cms list blocked");
87
+ const typeSlug = "article";
88
+
89
+ const plugin = cmsClientPlugin({
90
+ apiBaseURL: API_BASE_URL,
91
+ apiBasePath: API_BASE_PATH,
92
+ siteBaseURL: SITE_BASE_URL,
93
+ siteBasePath: SITE_BASE_PATH,
94
+ queryClient,
95
+ headers: TEST_HEADERS,
96
+ hooks: {
97
+ beforeLoadContentList: () => {
98
+ throw expectedError;
99
+ },
100
+ },
101
+ });
102
+
103
+ const route = plugin.routes().contentList({ params: { typeSlug } });
104
+ await route.loader?.();
105
+
106
+ const client = createApiClient<CMSApiRouter>({
107
+ baseURL: API_BASE_URL,
108
+ basePath: API_BASE_PATH,
109
+ });
110
+ const queries = createCMSQueryKeys(client, TEST_HEADERS);
111
+ const listQuery = queries.cmsContent.list({
112
+ typeSlug,
113
+ limit: 20,
114
+ offset: 0,
115
+ });
116
+
117
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
118
+ SSR_LOADER_ERROR_MESSAGE,
119
+ );
120
+ });
121
+
122
+ it("form list loader seeds query error when beforeLoadFormList throws", async () => {
123
+ const queryClient = new QueryClient();
124
+ const expectedError = new Error("form list blocked");
125
+
126
+ const plugin = formBuilderClientPlugin({
127
+ apiBaseURL: API_BASE_URL,
128
+ apiBasePath: API_BASE_PATH,
129
+ siteBaseURL: SITE_BASE_URL,
130
+ siteBasePath: SITE_BASE_PATH,
131
+ queryClient,
132
+ headers: TEST_HEADERS,
133
+ hooks: {
134
+ beforeLoadFormList: () => {
135
+ throw expectedError;
136
+ },
137
+ },
138
+ });
139
+
140
+ const route = plugin.routes().formList();
141
+ await route.loader?.();
142
+
143
+ const client = createApiClient<FormBuilderApiRouter>({
144
+ baseURL: API_BASE_URL,
145
+ basePath: API_BASE_PATH,
146
+ });
147
+ const queries = createFormBuilderQueryKeys(client, TEST_HEADERS);
148
+ const listQuery = queries.forms.list({ limit: 20, offset: 0 });
149
+
150
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
151
+ SSR_LOADER_ERROR_MESSAGE,
152
+ );
153
+ });
154
+
155
+ it("ui-builder list loader seeds query error when beforeLoadPageList throws", async () => {
156
+ const queryClient = new QueryClient();
157
+ const expectedError = new Error("ui-builder list blocked");
158
+
159
+ const plugin = uiBuilderClientPlugin({
160
+ apiBaseURL: API_BASE_URL,
161
+ apiBasePath: API_BASE_PATH,
162
+ siteBaseURL: SITE_BASE_URL,
163
+ siteBasePath: SITE_BASE_PATH,
164
+ queryClient,
165
+ headers: TEST_HEADERS,
166
+ componentRegistry: {},
167
+ hooks: {
168
+ beforeLoadPageList: () => {
169
+ throw expectedError;
170
+ },
171
+ },
172
+ });
173
+
174
+ const route = plugin.routes().pageList();
175
+ await route.loader?.();
176
+
177
+ const client = createApiClient<CMSApiRouter>({
178
+ baseURL: API_BASE_URL,
179
+ basePath: API_BASE_PATH,
180
+ });
181
+ const queries = createCMSQueryKeys(client, TEST_HEADERS);
182
+ const listQuery = queries.cmsContent.list({
183
+ typeSlug: UI_BUILDER_TYPE_SLUG,
184
+ limit: 20,
185
+ offset: 0,
186
+ });
187
+ const uiBuilderQueryKey = [...listQuery.queryKey, "ui-builder"] as const;
188
+
189
+ expect(getErrorMessage(queryClient, uiBuilderQueryKey)).toBe(
190
+ SSR_LOADER_ERROR_MESSAGE,
191
+ );
192
+ });
193
+
194
+ it("comments moderation loader seeds query error when beforeLoadModeration throws", async () => {
195
+ const queryClient = new QueryClient();
196
+ const expectedError = new Error("comments moderation blocked");
197
+
198
+ const plugin = commentsClientPlugin({
199
+ apiBaseURL: API_BASE_URL,
200
+ apiBasePath: API_BASE_PATH,
201
+ siteBaseURL: SITE_BASE_URL,
202
+ siteBasePath: SITE_BASE_PATH,
203
+ queryClient,
204
+ headers: TEST_HEADERS,
205
+ hooks: {
206
+ beforeLoadModeration: () => {
207
+ throw expectedError;
208
+ },
209
+ },
210
+ });
211
+
212
+ const route = plugin.routes().moderation();
213
+ await route.loader?.();
214
+
215
+ const client = createApiClient<CommentsApiRouter>({
216
+ baseURL: API_BASE_URL,
217
+ basePath: API_BASE_PATH,
218
+ });
219
+ const queries = createCommentsQueryKeys(client, TEST_HEADERS);
220
+ const listQuery = queries.comments.list({
221
+ status: "pending",
222
+ limit: 20,
223
+ offset: 0,
224
+ });
225
+
226
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
227
+ SSR_LOADER_ERROR_MESSAGE,
228
+ );
229
+ });
230
+
231
+ it("comments user loader seeds query error with user-scoped key when beforeLoadUserComments throws", async () => {
232
+ const queryClient = new QueryClient();
233
+ const expectedError = new Error("comments user view blocked");
234
+ const currentUserId = "user-123";
235
+
236
+ const plugin = commentsClientPlugin({
237
+ apiBaseURL: API_BASE_URL,
238
+ apiBasePath: API_BASE_PATH,
239
+ siteBaseURL: SITE_BASE_URL,
240
+ siteBasePath: SITE_BASE_PATH,
241
+ queryClient,
242
+ headers: TEST_HEADERS,
243
+ hooks: {
244
+ beforeLoadUserComments: (context) => {
245
+ context.currentUserId = currentUserId;
246
+ throw expectedError;
247
+ },
248
+ },
249
+ });
250
+
251
+ const route = plugin.routes().userComments();
252
+ await route.loader?.();
253
+
254
+ const client = createApiClient<CommentsApiRouter>({
255
+ baseURL: API_BASE_URL,
256
+ basePath: API_BASE_PATH,
257
+ });
258
+ const queries = createCommentsQueryKeys(client, TEST_HEADERS);
259
+ const listQuery = queries.comments.list({
260
+ authorId: currentUserId,
261
+ sort: "desc",
262
+ limit: 20,
263
+ offset: 0,
264
+ });
265
+
266
+ expect(getErrorMessage(queryClient, listQuery.queryKey)).toBe(
267
+ SSR_LOADER_ERROR_MESSAGE,
268
+ );
269
+ });
270
+
271
+ it("comments moderation loader calls onLoadError when prefetch stores API error", async () => {
272
+ const queryClient = new QueryClient();
273
+ const apiErrorMessage = "comments moderation api failed";
274
+ const onLoadError = vi.fn();
275
+ mockFetchApiError(apiErrorMessage);
276
+
277
+ const plugin = commentsClientPlugin({
278
+ apiBaseURL: API_BASE_URL,
279
+ apiBasePath: API_BASE_PATH,
280
+ siteBaseURL: SITE_BASE_URL,
281
+ siteBasePath: SITE_BASE_PATH,
282
+ queryClient,
283
+ headers: TEST_HEADERS,
284
+ hooks: {
285
+ onLoadError,
286
+ },
287
+ });
288
+
289
+ const route = plugin.routes().moderation();
290
+ await route.loader?.();
291
+
292
+ expect(onLoadError).toHaveBeenCalledTimes(1);
293
+ const [errorArg] = onLoadError.mock.calls[0] ?? [];
294
+ expect(errorArg).toBeInstanceOf(Error);
295
+ expect((errorArg as Error).message).toBe(apiErrorMessage);
296
+ });
297
+
298
+ it("comments user loader calls onLoadError when user-scoped prefetch stores API error", async () => {
299
+ const queryClient = new QueryClient();
300
+ const apiErrorMessage = "comments user api failed";
301
+ const onLoadError = vi.fn();
302
+ const currentUserId = "user-123";
303
+ mockFetchApiError(apiErrorMessage);
304
+
305
+ const plugin = commentsClientPlugin({
306
+ apiBaseURL: API_BASE_URL,
307
+ apiBasePath: API_BASE_PATH,
308
+ siteBaseURL: SITE_BASE_URL,
309
+ siteBasePath: SITE_BASE_PATH,
310
+ queryClient,
311
+ headers: TEST_HEADERS,
312
+ hooks: {
313
+ beforeLoadUserComments: (context) => {
314
+ context.currentUserId = currentUserId;
315
+ },
316
+ onLoadError,
317
+ },
318
+ });
319
+
320
+ const route = plugin.routes().userComments();
321
+ await route.loader?.();
322
+
323
+ expect(onLoadError).toHaveBeenCalledTimes(1);
324
+ const [errorArg, contextArg] = onLoadError.mock.calls[0] ?? [];
325
+ expect(errorArg).toBeInstanceOf(Error);
326
+ expect((errorArg as Error).message).toBe(apiErrorMessage);
327
+ expect(contextArg).toMatchObject({ currentUserId });
328
+ });
329
+ });
@@ -7,6 +7,7 @@ import {
7
7
  import { createRoute } from "@btst/yar";
8
8
  import type { ComponentType } from "react";
9
9
  import type { QueryClient } from "@tanstack/react-query";
10
+ import { createSanitizedSSRLoaderError } from "../../utils";
10
11
  import type { BlogApiRouter } from "../api";
11
12
  import { createBlogQueryKeys } from "../query-keys";
12
13
  import type { Post, SerializedPost, SerializedTag } from "../types";
@@ -183,6 +184,18 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
183
184
  apiBasePath,
184
185
  headers,
185
186
  };
187
+ const limit = 10;
188
+ const client = createApiClient<BlogApiRouter>({
189
+ baseURL: apiBaseURL,
190
+ basePath: apiBasePath,
191
+ });
192
+ // note: for a module not to be bundled with client, and to be shared by client and server we need to add it to build.config.ts as an entry
193
+ const queries = createBlogQueryKeys(client, headers);
194
+ const listQuery = queries.posts.list({
195
+ query: undefined,
196
+ limit,
197
+ published: published,
198
+ });
186
199
 
187
200
  try {
188
201
  // Before hook
@@ -193,20 +206,6 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
193
206
  );
194
207
  }
195
208
 
196
- const limit = 10;
197
- const client = createApiClient<BlogApiRouter>({
198
- baseURL: apiBaseURL,
199
- basePath: apiBasePath,
200
- });
201
-
202
- // note: for a module not to be bundled with client, and to be shared by client and server we need to add it to build.config.ts as an entry
203
- const queries = createBlogQueryKeys(client, headers);
204
- const listQuery = queries.posts.list({
205
- query: undefined,
206
- limit,
207
- published: published,
208
- });
209
-
210
209
  await queryClient.prefetchInfiniteQuery({
211
210
  ...listQuery,
212
211
  initialPageParam: 0,
@@ -250,6 +249,16 @@ function createPostsLoader(published: boolean, config: BlogClientConfig) {
250
249
  "[btst/blog] route.loader() failed — no server running at build time. " +
251
250
  "Use myStack.api.blog.prefetchForRoute() for SSG data prefetching.",
252
251
  );
252
+ } else {
253
+ const errToStore = createSanitizedSSRLoaderError();
254
+ await queryClient.prefetchInfiniteQuery({
255
+ queryKey: listQuery.queryKey,
256
+ queryFn: () => {
257
+ throw errToStore;
258
+ },
259
+ initialPageParam: 0,
260
+ retry: false,
261
+ });
253
262
  }
254
263
  if (hooks?.onLoadError) {
255
264
  await hooks.onLoadError(error as Error, context);
@@ -20,8 +20,10 @@ export type {
20
20
 
21
21
  export {
22
22
  createApiClient,
23
+ createSanitizedSSRLoaderError,
23
24
  isConnectionError,
24
25
  runClientHookWithShim,
26
+ SSR_LOADER_ERROR_MESSAGE,
25
27
  } from "../utils";
26
28
 
27
29
  // Re-export Yar types needed for plugins