@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
@@ -8,6 +8,7 @@ import {
8
8
  import { createRoute } from "@btst/yar";
9
9
  import type { ComponentType } from "react";
10
10
  import type { QueryClient } from "@tanstack/react-query";
11
+ import { createSanitizedSSRLoaderError } from "../../utils";
11
12
  import type { CMSApiRouter } from "../api";
12
13
  import { createCMSQueryKeys } from "../query-keys";
13
14
 
@@ -163,6 +164,12 @@ function createDashboardLoader(config: CMSClientConfig) {
163
164
  apiBasePath,
164
165
  headers,
165
166
  };
167
+ const client = createApiClient<CMSApiRouter>({
168
+ baseURL: apiBaseURL,
169
+ basePath: apiBasePath,
170
+ });
171
+ const queries = createCMSQueryKeys(client, headers);
172
+ const typesQuery = queries.cmsTypes.list();
166
173
 
167
174
  try {
168
175
  // Before hook - authorization check
@@ -173,13 +180,7 @@ function createDashboardLoader(config: CMSClientConfig) {
173
180
  );
174
181
  }
175
182
 
176
- const client = createApiClient<CMSApiRouter>({
177
- baseURL: apiBaseURL,
178
- basePath: apiBasePath,
179
- });
180
- const queries = createCMSQueryKeys(client, headers);
181
-
182
- await queryClient.prefetchQuery(queries.cmsTypes.list());
183
+ await queryClient.prefetchQuery(typesQuery);
183
184
 
184
185
  // After hook
185
186
  if (hooks?.afterLoadDashboard) {
@@ -187,9 +188,7 @@ function createDashboardLoader(config: CMSClientConfig) {
187
188
  }
188
189
 
189
190
  // Check if there was an error
190
- const queryState = queryClient.getQueryState(
191
- queries.cmsTypes.list().queryKey,
192
- );
191
+ const queryState = queryClient.getQueryState(typesQuery.queryKey);
193
192
  if (queryState?.error && hooks?.onLoadError) {
194
193
  const error =
195
194
  queryState.error instanceof Error
@@ -205,6 +204,15 @@ function createDashboardLoader(config: CMSClientConfig) {
205
204
  "[btst/cms] route.loader() failed — no server running at build time. " +
206
205
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
207
206
  );
207
+ } else {
208
+ const errToStore = createSanitizedSSRLoaderError();
209
+ await queryClient.prefetchQuery({
210
+ queryKey: typesQuery.queryKey,
211
+ queryFn: () => {
212
+ throw errToStore;
213
+ },
214
+ retry: false,
215
+ });
208
216
  }
209
217
  if (hooks?.onLoadError) {
210
218
  await hooks.onLoadError(error as Error, context);
@@ -231,6 +239,18 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
231
239
  apiBasePath,
232
240
  headers,
233
241
  };
242
+ const client = createApiClient<CMSApiRouter>({
243
+ baseURL: apiBaseURL,
244
+ basePath: apiBasePath,
245
+ });
246
+ const queries = createCMSQueryKeys(client, headers);
247
+ const limit = 20;
248
+ const typesQuery = queries.cmsTypes.list();
249
+ const listQuery = queries.cmsContent.list({
250
+ typeSlug,
251
+ limit,
252
+ offset: 0,
253
+ });
234
254
 
235
255
  try {
236
256
  // Before hook - authorization check
@@ -241,22 +261,10 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
241
261
  );
242
262
  }
243
263
 
244
- const client = createApiClient<CMSApiRouter>({
245
- baseURL: apiBaseURL,
246
- basePath: apiBasePath,
247
- });
248
- const queries = createCMSQueryKeys(client, headers);
249
- const limit = 20;
250
-
251
264
  // Prefetch content types
252
- await queryClient.prefetchQuery(queries.cmsTypes.list());
265
+ await queryClient.prefetchQuery(typesQuery);
253
266
 
254
267
  // Prefetch content list using infinite query (matches useSuspenseInfiniteQuery in hooks)
255
- const listQuery = queries.cmsContent.list({
256
- typeSlug,
257
- limit,
258
- offset: 0,
259
- });
260
268
  await queryClient.prefetchInfiniteQuery({
261
269
  queryKey: listQuery.queryKey,
262
270
  queryFn: async ({ pageParam = 0 }) => {
@@ -285,9 +293,7 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
285
293
  }
286
294
 
287
295
  // Check if there was an error in either query
288
- const typesState = queryClient.getQueryState(
289
- queries.cmsTypes.list().queryKey,
290
- );
296
+ const typesState = queryClient.getQueryState(typesQuery.queryKey);
291
297
  const listState = queryClient.getQueryState(listQuery.queryKey);
292
298
  const queryError = typesState?.error || listState?.error;
293
299
  if (queryError && hooks?.onLoadError) {
@@ -305,6 +311,16 @@ function createContentListLoader(typeSlug: string, config: CMSClientConfig) {
305
311
  "[btst/cms] route.loader() failed — no server running at build time. " +
306
312
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
307
313
  );
314
+ } else {
315
+ const errToStore = createSanitizedSSRLoaderError();
316
+ await queryClient.prefetchInfiniteQuery({
317
+ queryKey: listQuery.queryKey,
318
+ queryFn: () => {
319
+ throw errToStore;
320
+ },
321
+ initialPageParam: 0,
322
+ retry: false,
323
+ });
308
324
  }
309
325
  if (hooks?.onLoadError) {
310
326
  await hooks.onLoadError(error as Error, context);
@@ -335,6 +351,15 @@ function createContentEditorLoader(
335
351
  apiBasePath,
336
352
  headers,
337
353
  };
354
+ const client = createApiClient<CMSApiRouter>({
355
+ baseURL: apiBaseURL,
356
+ basePath: apiBasePath,
357
+ });
358
+ const queries = createCMSQueryKeys(client, headers);
359
+ const typesQuery = queries.cmsTypes.list();
360
+ const detailQuery = id
361
+ ? queries.cmsContent.detail(typeSlug, id)
362
+ : undefined;
338
363
 
339
364
  try {
340
365
  // Before hook - authorization check
@@ -345,17 +370,9 @@ function createContentEditorLoader(
345
370
  );
346
371
  }
347
372
 
348
- const client = createApiClient<CMSApiRouter>({
349
- baseURL: apiBaseURL,
350
- basePath: apiBasePath,
351
- });
352
- const queries = createCMSQueryKeys(client, headers);
353
-
354
- const promises = [queryClient.prefetchQuery(queries.cmsTypes.list())];
373
+ const promises = [queryClient.prefetchQuery(typesQuery)];
355
374
  if (id) {
356
- promises.push(
357
- queryClient.prefetchQuery(queries.cmsContent.detail(typeSlug, id)),
358
- );
375
+ promises.push(queryClient.prefetchQuery(detailQuery!));
359
376
  }
360
377
  await Promise.all(promises);
361
378
 
@@ -365,13 +382,9 @@ function createContentEditorLoader(
365
382
  }
366
383
 
367
384
  // Check if there was an error
368
- const typesState = queryClient.getQueryState(
369
- queries.cmsTypes.list().queryKey,
370
- );
385
+ const typesState = queryClient.getQueryState(typesQuery.queryKey);
371
386
  const itemState = id
372
- ? queryClient.getQueryState(
373
- queries.cmsContent.detail(typeSlug, id).queryKey,
374
- )
387
+ ? queryClient.getQueryState(detailQuery!.queryKey)
375
388
  : null;
376
389
  const queryError = typesState?.error || itemState?.error;
377
390
  if (queryError && hooks?.onLoadError) {
@@ -389,6 +402,24 @@ function createContentEditorLoader(
389
402
  "[btst/cms] route.loader() failed — no server running at build time. " +
390
403
  "Use myStack.api.cms.prefetchForRoute() for SSG data prefetching.",
391
404
  );
405
+ } else {
406
+ const errToStore = createSanitizedSSRLoaderError();
407
+ await queryClient.prefetchQuery({
408
+ queryKey: typesQuery.queryKey,
409
+ queryFn: () => {
410
+ throw errToStore;
411
+ },
412
+ retry: false,
413
+ });
414
+ if (detailQuery) {
415
+ await queryClient.prefetchQuery({
416
+ queryKey: detailQuery.queryKey,
417
+ queryFn: () => {
418
+ throw errToStore;
419
+ },
420
+ retry: false,
421
+ });
422
+ }
392
423
  }
393
424
  if (hooks?.onLoadError) {
394
425
  await hooks.onLoadError(error as Error, context);
@@ -2,10 +2,14 @@
2
2
  import { lazy } from "react";
3
3
  import {
4
4
  defineClientPlugin,
5
+ createApiClient,
5
6
  isConnectionError,
6
7
  } from "@btst/stack/plugins/client";
7
8
  import { createRoute } from "@btst/yar";
8
9
  import type { QueryClient } from "@tanstack/react-query";
10
+ import type { CommentsApiRouter } from "../api";
11
+ import { createCommentsQueryKeys } from "../query-keys";
12
+ import { createSanitizedSSRLoaderError } from "../../utils";
9
13
 
10
14
  // Lazy load page components for code splitting
11
15
  const ModerationPageComponent = lazy(() =>
@@ -36,6 +40,11 @@ export interface LoaderContext {
36
40
  apiBasePath: string;
37
41
  /** Optional headers for the request */
38
42
  headers?: Headers;
43
+ /**
44
+ * Optional current user ID for SSR loaders that need user-scoped query keys.
45
+ * Hooks (e.g. beforeLoadUserComments) may populate this.
46
+ */
47
+ currentUserId?: string;
39
48
  /** Additional context properties */
40
49
  [key: string]: unknown;
41
50
  }
@@ -81,7 +90,7 @@ export interface CommentsClientConfig {
81
90
  function createModerationLoader(config: CommentsClientConfig) {
82
91
  return async () => {
83
92
  if (typeof window === "undefined") {
84
- const { apiBasePath, apiBaseURL, headers, hooks } = config;
93
+ const { queryClient, apiBasePath, apiBaseURL, headers, hooks } = config;
85
94
  const context: LoaderContext = {
86
95
  path: "/comments/moderation",
87
96
  isSSR: true,
@@ -89,15 +98,43 @@ function createModerationLoader(config: CommentsClientConfig) {
89
98
  apiBasePath,
90
99
  headers,
91
100
  };
101
+ const client = createApiClient<CommentsApiRouter>({
102
+ baseURL: apiBaseURL,
103
+ basePath: apiBasePath,
104
+ });
105
+ const queries = createCommentsQueryKeys(client, headers);
106
+ const listQuery = queries.comments.list({
107
+ status: "pending",
108
+ limit: 20,
109
+ offset: 0,
110
+ });
92
111
  try {
93
112
  if (hooks?.beforeLoadModeration) {
94
113
  await hooks.beforeLoadModeration(context);
95
114
  }
115
+ await queryClient.prefetchQuery(listQuery);
116
+ const queryState = queryClient.getQueryState(listQuery.queryKey);
117
+ if (queryState?.error && hooks?.onLoadError) {
118
+ const error =
119
+ queryState.error instanceof Error
120
+ ? queryState.error
121
+ : new Error(String(queryState.error));
122
+ await hooks.onLoadError(error, context);
123
+ }
96
124
  } catch (error) {
97
125
  if (isConnectionError(error)) {
98
126
  console.warn(
99
127
  "[btst/comments] route.loader() failed — no server running at build time.",
100
128
  );
129
+ } else {
130
+ const errToStore = createSanitizedSSRLoaderError();
131
+ await queryClient.prefetchQuery({
132
+ queryKey: listQuery.queryKey,
133
+ queryFn: () => {
134
+ throw errToStore;
135
+ },
136
+ retry: false,
137
+ });
101
138
  }
102
139
  if (hooks?.onLoadError) {
103
140
  await hooks.onLoadError(error as Error, context);
@@ -110,7 +147,7 @@ function createModerationLoader(config: CommentsClientConfig) {
110
147
  function createUserCommentsLoader(config: CommentsClientConfig) {
111
148
  return async () => {
112
149
  if (typeof window === "undefined") {
113
- const { apiBasePath, apiBaseURL, headers, hooks } = config;
150
+ const { queryClient, apiBasePath, apiBaseURL, headers, hooks } = config;
114
151
  const context: LoaderContext = {
115
152
  path: "/comments",
116
153
  isSSR: true,
@@ -118,15 +155,58 @@ function createUserCommentsLoader(config: CommentsClientConfig) {
118
155
  apiBasePath,
119
156
  headers,
120
157
  };
158
+ const client = createApiClient<CommentsApiRouter>({
159
+ baseURL: apiBaseURL,
160
+ basePath: apiBasePath,
161
+ });
162
+ const queries = createCommentsQueryKeys(client, headers);
163
+ const getUserListQuery = (currentUserId: string) =>
164
+ queries.comments.list({
165
+ authorId: currentUserId,
166
+ sort: "desc",
167
+ limit: 20,
168
+ offset: 0,
169
+ });
121
170
  try {
122
171
  if (hooks?.beforeLoadUserComments) {
123
172
  await hooks.beforeLoadUserComments(context);
124
173
  }
174
+ const currentUserId =
175
+ typeof context.currentUserId === "string"
176
+ ? context.currentUserId
177
+ : undefined;
178
+ if (currentUserId) {
179
+ const listQuery = getUserListQuery(currentUserId);
180
+ await queryClient.prefetchQuery(listQuery);
181
+ const queryState = queryClient.getQueryState(listQuery.queryKey);
182
+ if (queryState?.error && hooks?.onLoadError) {
183
+ const error =
184
+ queryState.error instanceof Error
185
+ ? queryState.error
186
+ : new Error(String(queryState.error));
187
+ await hooks.onLoadError(error, context);
188
+ }
189
+ }
125
190
  } catch (error) {
126
191
  if (isConnectionError(error)) {
127
192
  console.warn(
128
193
  "[btst/comments] route.loader() failed — no server running at build time.",
129
194
  );
195
+ } else {
196
+ const currentUserId =
197
+ typeof context.currentUserId === "string"
198
+ ? context.currentUserId
199
+ : undefined;
200
+ if (currentUserId) {
201
+ const errToStore = createSanitizedSSRLoaderError();
202
+ await queryClient.prefetchQuery({
203
+ queryKey: getUserListQuery(currentUserId).queryKey,
204
+ queryFn: () => {
205
+ throw errToStore;
206
+ },
207
+ retry: false,
208
+ });
209
+ }
130
210
  }
131
211
  if (hooks?.onLoadError) {
132
212
  await hooks.onLoadError(error as Error, context);
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect } from "react";
2
2
  import type { CommentsPluginOverrides } from "./overrides";
3
+ import { toError as toErrorShared } from "../error-utils";
3
4
 
4
5
  /**
5
6
  * Resolves `currentUserId` from the plugin overrides, supporting both a static
@@ -40,20 +41,7 @@ export function useResolvedCurrentUserId(
40
41
  * copied onto the Error via Object.assign so callers can inspect them.
41
42
  * 3. Anything else — converted via String().
42
43
  */
43
- export function toError(error: unknown): Error {
44
- if (error instanceof Error) return error;
45
- if (typeof error === "object" && error !== null) {
46
- const obj = error as Record<string, unknown>;
47
- const message =
48
- (typeof obj.message === "string" ? obj.message : null) ||
49
- (typeof obj.error === "string" ? obj.error : null) ||
50
- JSON.stringify(error);
51
- const err = new Error(message);
52
- Object.assign(err, error);
53
- return err;
54
- }
55
- return new Error(String(error));
56
- }
44
+ export const toError = toErrorShared;
57
45
 
58
46
  export function getInitials(name: string | null | undefined): string {
59
47
  if (!name) return "?";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Normalize any thrown value into an Error.
3
+ */
4
+ export function toError(error: unknown): Error {
5
+ if (error instanceof Error) return error;
6
+ if (typeof error === "object" && error !== null) {
7
+ const obj = error as Record<string, unknown>;
8
+ const message =
9
+ (typeof obj.message === "string" ? obj.message : null) ||
10
+ (typeof obj.error === "string" ? obj.error : null) ||
11
+ JSON.stringify(error);
12
+ const err = new Error(message);
13
+ Object.assign(err, error);
14
+ return err;
15
+ }
16
+ return new Error(String(error));
17
+ }
@@ -10,7 +10,7 @@ import {
10
10
  commentCountDiscriminator,
11
11
  commentsThreadDiscriminator,
12
12
  } from "./api/query-key-defs";
13
- import { toError } from "./client/utils";
13
+ import { toError } from "./error-utils";
14
14
 
15
15
  interface CommentsListParams {
16
16
  resourceId?: string;
@@ -9,6 +9,7 @@ import {
9
9
  import { createRoute } from "@btst/yar";
10
10
  import type { ComponentType } from "react";
11
11
  import type { QueryClient } from "@tanstack/react-query";
12
+ import { createSanitizedSSRLoaderError } from "../../utils";
12
13
  import type { FormBuilderApiRouter } from "../api";
13
14
  import { createFormBuilderQueryKeys } from "../query-keys";
14
15
 
@@ -160,6 +161,13 @@ function createFormListLoader(config: FormBuilderClientConfig) {
160
161
  apiBasePath,
161
162
  headers,
162
163
  };
164
+ const client = createApiClient<FormBuilderApiRouter>({
165
+ baseURL: apiBaseURL,
166
+ basePath: apiBasePath,
167
+ });
168
+ const queries = createFormBuilderQueryKeys(client, headers);
169
+ const limit = 20;
170
+ const listQuery = queries.forms.list({ limit, offset: 0 });
163
171
 
164
172
  try {
165
173
  // Before hook - authorization check
@@ -170,15 +178,7 @@ function createFormListLoader(config: FormBuilderClientConfig) {
170
178
  );
171
179
  }
172
180
 
173
- const client = createApiClient<FormBuilderApiRouter>({
174
- baseURL: apiBaseURL,
175
- basePath: apiBasePath,
176
- });
177
- const queries = createFormBuilderQueryKeys(client, headers);
178
- const limit = 20;
179
-
180
181
  // Prefetch forms using infinite query
181
- const listQuery = queries.forms.list({ limit, offset: 0 });
182
182
  await queryClient.prefetchInfiniteQuery({
183
183
  queryKey: listQuery.queryKey,
184
184
  queryFn: async ({ pageParam = 0 }) => {
@@ -221,6 +221,16 @@ function createFormListLoader(config: FormBuilderClientConfig) {
221
221
  "[btst/form-builder] route.loader() failed — no server running at build time. " +
222
222
  "Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
223
223
  );
224
+ } else {
225
+ const errToStore = createSanitizedSSRLoaderError();
226
+ await queryClient.prefetchInfiniteQuery({
227
+ queryKey: listQuery.queryKey,
228
+ queryFn: () => {
229
+ throw errToStore;
230
+ },
231
+ initialPageParam: 0,
232
+ retry: false,
233
+ });
224
234
  }
225
235
  if (hooks?.onLoadError) {
226
236
  await hooks.onLoadError(error as Error, context);
@@ -249,6 +259,12 @@ function createFormBuilderLoader(
249
259
  apiBasePath,
250
260
  headers,
251
261
  };
262
+ const client = createApiClient<FormBuilderApiRouter>({
263
+ baseURL: apiBaseURL,
264
+ basePath: apiBasePath,
265
+ });
266
+ const queries = createFormBuilderQueryKeys(client, headers);
267
+ const formQuery = id ? queries.forms.byId(id) : undefined;
252
268
 
253
269
  try {
254
270
  // Before hook - authorization check
@@ -259,15 +275,9 @@ function createFormBuilderLoader(
259
275
  );
260
276
  }
261
277
 
262
- const client = createApiClient<FormBuilderApiRouter>({
263
- baseURL: apiBaseURL,
264
- basePath: apiBasePath,
265
- });
266
- const queries = createFormBuilderQueryKeys(client, headers);
267
-
268
278
  // Prefetch form if editing
269
279
  if (id) {
270
- await queryClient.prefetchQuery(queries.forms.byId(id));
280
+ await queryClient.prefetchQuery(formQuery!);
271
281
  }
272
282
 
273
283
  // After hook
@@ -277,9 +287,7 @@ function createFormBuilderLoader(
277
287
 
278
288
  // Check if there was an error
279
289
  if (id) {
280
- const queryState = queryClient.getQueryState(
281
- queries.forms.byId(id).queryKey,
282
- );
290
+ const queryState = queryClient.getQueryState(formQuery!.queryKey);
283
291
  if (queryState?.error && hooks?.onLoadError) {
284
292
  const error =
285
293
  queryState.error instanceof Error
@@ -295,6 +303,15 @@ function createFormBuilderLoader(
295
303
  "[btst/form-builder] route.loader() failed — no server running at build time. " +
296
304
  "Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
297
305
  );
306
+ } else if (formQuery) {
307
+ const errToStore = createSanitizedSSRLoaderError();
308
+ await queryClient.prefetchQuery({
309
+ queryKey: formQuery.queryKey,
310
+ queryFn: () => {
311
+ throw errToStore;
312
+ },
313
+ retry: false,
314
+ });
298
315
  }
299
316
  if (hooks?.onLoadError) {
300
317
  await hooks.onLoadError(error as Error, context);
@@ -323,6 +340,18 @@ function createSubmissionsLoader(
323
340
  apiBasePath,
324
341
  headers,
325
342
  };
343
+ const client = createApiClient<FormBuilderApiRouter>({
344
+ baseURL: apiBaseURL,
345
+ basePath: apiBasePath,
346
+ });
347
+ const queries = createFormBuilderQueryKeys(client, headers);
348
+ const limit = 20;
349
+ const formQuery = queries.forms.byId(formId);
350
+ const submissionsQuery = queries.formSubmissions.list({
351
+ formId,
352
+ limit,
353
+ offset: 0,
354
+ });
326
355
 
327
356
  try {
328
357
  // Before hook - authorization check
@@ -333,21 +362,8 @@ function createSubmissionsLoader(
333
362
  );
334
363
  }
335
364
 
336
- const client = createApiClient<FormBuilderApiRouter>({
337
- baseURL: apiBaseURL,
338
- basePath: apiBasePath,
339
- });
340
- const queries = createFormBuilderQueryKeys(client, headers);
341
- const limit = 20;
342
-
343
365
  // Prefetch form and submissions
344
- await queryClient.prefetchQuery(queries.forms.byId(formId));
345
-
346
- const submissionsQuery = queries.formSubmissions.list({
347
- formId,
348
- limit,
349
- offset: 0,
350
- });
366
+ await queryClient.prefetchQuery(formQuery);
351
367
  await queryClient.prefetchInfiniteQuery({
352
368
  queryKey: submissionsQuery.queryKey,
353
369
  queryFn: async ({ pageParam = 0 }) => {
@@ -379,9 +395,7 @@ function createSubmissionsLoader(
379
395
  }
380
396
 
381
397
  // Check if there was an error
382
- const formState = queryClient.getQueryState(
383
- queries.forms.byId(formId).queryKey,
384
- );
398
+ const formState = queryClient.getQueryState(formQuery.queryKey);
385
399
  const submissionsState = queryClient.getQueryState(
386
400
  submissionsQuery.queryKey,
387
401
  );
@@ -400,6 +414,16 @@ function createSubmissionsLoader(
400
414
  "[btst/form-builder] route.loader() failed — no server running at build time. " +
401
415
  "Use myStack.api.formBuilder.prefetchForRoute() for SSG data prefetching.",
402
416
  );
417
+ } else {
418
+ const errToStore = createSanitizedSSRLoaderError();
419
+ await queryClient.prefetchInfiniteQuery({
420
+ queryKey: submissionsQuery.queryKey,
421
+ queryFn: () => {
422
+ throw errToStore;
423
+ },
424
+ initialPageParam: 0,
425
+ retry: false,
426
+ });
403
427
  }
404
428
  if (hooks?.onLoadError) {
405
429
  await hooks.onLoadError(error as Error, context);