@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.
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +22 -11
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +22 -11
- package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +71 -39
- package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +71 -39
- package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +62 -2
- package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +63 -3
- package/dist/packages/stack/src/plugins/comments/client/utils.cjs +2 -11
- package/dist/packages/stack/src/plugins/comments/client/utils.mjs +2 -11
- package/dist/packages/stack/src/plugins/comments/error-utils.cjs +15 -0
- package/dist/packages/stack/src/plugins/comments/error-utils.mjs +13 -0
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +59 -31
- package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +59 -31
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +52 -25
- package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +53 -26
- package/dist/packages/stack/src/plugins/utils.cjs +6 -0
- package/dist/packages/stack/src/plugins/utils.mjs +5 -1
- package/dist/plugins/client/index.cjs +2 -0
- package/dist/plugins/client/index.d.cts +15 -1
- package/dist/plugins/client/index.d.mts +15 -1
- package/dist/plugins/client/index.d.ts +15 -1
- package/dist/plugins/client/index.mjs +1 -1
- package/dist/plugins/comments/client/index.d.cts +5 -0
- package/dist/plugins/comments/client/index.d.mts +5 -0
- package/dist/plugins/comments/client/index.d.ts +5 -0
- package/dist/plugins/comments/query-keys.cjs +4 -4
- package/dist/plugins/comments/query-keys.mjs +1 -1
- package/package.json +1 -1
- package/src/__tests__/client-plugin-ssr-loaders.test.ts +329 -0
- package/src/plugins/blog/client/plugin.tsx +23 -14
- package/src/plugins/client/index.ts +2 -0
- package/src/plugins/cms/client/plugin.tsx +73 -42
- package/src/plugins/comments/client/plugin.tsx +82 -2
- package/src/plugins/comments/client/utils.ts +2 -14
- package/src/plugins/comments/error-utils.ts +17 -0
- package/src/plugins/comments/query-keys.ts +1 -1
- package/src/plugins/form-builder/client/plugin.tsx +59 -35
- package/src/plugins/ui-builder/client/plugin.tsx +57 -27
- package/src/plugins/utils.ts +18 -0
|
@@ -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
|
|
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
|
+
}
|
|
@@ -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(
|
|
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(
|
|
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);
|
|
@@ -3,6 +3,7 @@ import { lazy } from "react";
|
|
|
3
3
|
import {
|
|
4
4
|
defineClientPlugin,
|
|
5
5
|
createApiClient,
|
|
6
|
+
isConnectionError,
|
|
6
7
|
runClientHookWithShim,
|
|
7
8
|
} from "@btst/stack/plugins/client";
|
|
8
9
|
import { createRoute } from "@btst/yar";
|
|
@@ -10,6 +11,7 @@ import type { ComponentType } from "react";
|
|
|
10
11
|
import type { QueryClient } from "@tanstack/react-query";
|
|
11
12
|
import type { CMSApiRouter } from "../../cms/api";
|
|
12
13
|
import { createCMSQueryKeys } from "../../cms/query-keys";
|
|
14
|
+
import { createSanitizedSSRLoaderError } from "../../utils";
|
|
13
15
|
import { UI_BUILDER_TYPE_SLUG } from "../schemas";
|
|
14
16
|
import type {
|
|
15
17
|
UIBuilderClientHooks,
|
|
@@ -81,6 +83,18 @@ function createPageListLoader(config: UIBuilderClientConfig) {
|
|
|
81
83
|
apiBasePath,
|
|
82
84
|
headers,
|
|
83
85
|
};
|
|
86
|
+
const client = createApiClient<CMSApiRouter>({
|
|
87
|
+
baseURL: apiBaseURL,
|
|
88
|
+
basePath: apiBasePath,
|
|
89
|
+
});
|
|
90
|
+
const queries = createCMSQueryKeys(client, headers);
|
|
91
|
+
const limit = 20;
|
|
92
|
+
const listQuery = queries.cmsContent.list({
|
|
93
|
+
typeSlug,
|
|
94
|
+
limit,
|
|
95
|
+
offset: 0,
|
|
96
|
+
});
|
|
97
|
+
const uiBuilderListQueryKey = [...listQuery.queryKey, "ui-builder"];
|
|
84
98
|
|
|
85
99
|
try {
|
|
86
100
|
// Before hook - authorization check
|
|
@@ -91,21 +105,9 @@ function createPageListLoader(config: UIBuilderClientConfig) {
|
|
|
91
105
|
);
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
const client = createApiClient<CMSApiRouter>({
|
|
95
|
-
baseURL: apiBaseURL,
|
|
96
|
-
basePath: apiBasePath,
|
|
97
|
-
});
|
|
98
|
-
const queries = createCMSQueryKeys(client, headers);
|
|
99
|
-
const limit = 20;
|
|
100
|
-
|
|
101
108
|
// Prefetch pages using infinite query
|
|
102
|
-
const listQuery = queries.cmsContent.list({
|
|
103
|
-
typeSlug,
|
|
104
|
-
limit,
|
|
105
|
-
offset: 0,
|
|
106
|
-
});
|
|
107
109
|
await queryClient.prefetchInfiniteQuery({
|
|
108
|
-
queryKey:
|
|
110
|
+
queryKey: uiBuilderListQueryKey,
|
|
109
111
|
queryFn: async ({ pageParam = 0 }) => {
|
|
110
112
|
const response: unknown = await client("/content/:typeSlug", {
|
|
111
113
|
method: "GET",
|
|
@@ -133,8 +135,7 @@ function createPageListLoader(config: UIBuilderClientConfig) {
|
|
|
133
135
|
|
|
134
136
|
// Check if there was an error
|
|
135
137
|
const queryState = queryClient.getQueryState([
|
|
136
|
-
...
|
|
137
|
-
"ui-builder",
|
|
138
|
+
...uiBuilderListQueryKey,
|
|
138
139
|
]);
|
|
139
140
|
if (queryState?.error && hooks?.onLoadError) {
|
|
140
141
|
const error =
|
|
@@ -145,6 +146,22 @@ function createPageListLoader(config: UIBuilderClientConfig) {
|
|
|
145
146
|
}
|
|
146
147
|
} catch (error) {
|
|
147
148
|
// Error hook - log the error but don't throw during SSR
|
|
149
|
+
if (isConnectionError(error)) {
|
|
150
|
+
console.warn(
|
|
151
|
+
"[btst/ui-builder] route.loader() failed — no server running at build time. " +
|
|
152
|
+
"Use myStack.api.uiBuilder.prefetchForRoute() for SSG data prefetching.",
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
const errToStore = createSanitizedSSRLoaderError();
|
|
156
|
+
await queryClient.prefetchInfiniteQuery({
|
|
157
|
+
queryKey: uiBuilderListQueryKey,
|
|
158
|
+
queryFn: () => {
|
|
159
|
+
throw errToStore;
|
|
160
|
+
},
|
|
161
|
+
initialPageParam: 0,
|
|
162
|
+
retry: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
148
165
|
if (hooks?.onLoadError) {
|
|
149
166
|
await hooks.onLoadError(error as Error, context);
|
|
150
167
|
}
|
|
@@ -173,6 +190,14 @@ function createPageBuilderLoader(
|
|
|
173
190
|
apiBasePath,
|
|
174
191
|
headers,
|
|
175
192
|
};
|
|
193
|
+
const client = createApiClient<CMSApiRouter>({
|
|
194
|
+
baseURL: apiBaseURL,
|
|
195
|
+
basePath: apiBasePath,
|
|
196
|
+
});
|
|
197
|
+
const queries = createCMSQueryKeys(client, headers);
|
|
198
|
+
const pageQuery = id
|
|
199
|
+
? queries.cmsContent.detail(typeSlug, id)
|
|
200
|
+
: undefined;
|
|
176
201
|
|
|
177
202
|
try {
|
|
178
203
|
// Before hook - authorization check
|
|
@@ -183,17 +208,9 @@ function createPageBuilderLoader(
|
|
|
183
208
|
);
|
|
184
209
|
}
|
|
185
210
|
|
|
186
|
-
const client = createApiClient<CMSApiRouter>({
|
|
187
|
-
baseURL: apiBaseURL,
|
|
188
|
-
basePath: apiBasePath,
|
|
189
|
-
});
|
|
190
|
-
const queries = createCMSQueryKeys(client, headers);
|
|
191
|
-
|
|
192
211
|
// Prefetch page if editing
|
|
193
212
|
if (id) {
|
|
194
|
-
await queryClient.prefetchQuery(
|
|
195
|
-
queries.cmsContent.detail(typeSlug, id),
|
|
196
|
-
);
|
|
213
|
+
await queryClient.prefetchQuery(pageQuery!);
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
// After hook
|
|
@@ -203,9 +220,7 @@ function createPageBuilderLoader(
|
|
|
203
220
|
|
|
204
221
|
// Check if there was an error
|
|
205
222
|
if (id) {
|
|
206
|
-
const queryState = queryClient.getQueryState(
|
|
207
|
-
queries.cmsContent.detail(typeSlug, id).queryKey,
|
|
208
|
-
);
|
|
223
|
+
const queryState = queryClient.getQueryState(pageQuery!.queryKey);
|
|
209
224
|
if (queryState?.error && hooks?.onLoadError) {
|
|
210
225
|
const error =
|
|
211
226
|
queryState.error instanceof Error
|
|
@@ -216,6 +231,21 @@ function createPageBuilderLoader(
|
|
|
216
231
|
}
|
|
217
232
|
} catch (error) {
|
|
218
233
|
// Error hook - log the error but don't throw during SSR
|
|
234
|
+
if (isConnectionError(error)) {
|
|
235
|
+
console.warn(
|
|
236
|
+
"[btst/ui-builder] route.loader() failed — no server running at build time. " +
|
|
237
|
+
"Use myStack.api.uiBuilder.prefetchForRoute() for SSG data prefetching.",
|
|
238
|
+
);
|
|
239
|
+
} else if (pageQuery) {
|
|
240
|
+
const errToStore = createSanitizedSSRLoaderError();
|
|
241
|
+
await queryClient.prefetchQuery({
|
|
242
|
+
queryKey: pageQuery.queryKey,
|
|
243
|
+
queryFn: () => {
|
|
244
|
+
throw errToStore;
|
|
245
|
+
},
|
|
246
|
+
retry: false,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
219
249
|
if (hooks?.onLoadError) {
|
|
220
250
|
await hooks.onLoadError(error as Error, context);
|
|
221
251
|
}
|
package/src/plugins/utils.ts
CHANGED
|
@@ -109,6 +109,24 @@ export function isConnectionError(err: unknown): boolean {
|
|
|
109
109
|
code === "ERR_CONNECTION_REFUSED"
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Public-safe message used when SSR loader failures are intentionally seeded
|
|
115
|
+
* into React Query so Error Boundaries can render on the client.
|
|
116
|
+
*
|
|
117
|
+
* Never include raw server error text here because dehydrated query state can
|
|
118
|
+
* be serialized into HTML.
|
|
119
|
+
*/
|
|
120
|
+
export const SSR_LOADER_ERROR_MESSAGE = "Failed to load data.";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Creates a sanitized Error for SSR loader cache seeding.
|
|
124
|
+
*
|
|
125
|
+
* Use this instead of storing raw server errors in dehydrated query state.
|
|
126
|
+
*/
|
|
127
|
+
export function createSanitizedSSRLoaderError(): Error {
|
|
128
|
+
return new Error(SSR_LOADER_ERROR_MESSAGE);
|
|
129
|
+
}
|
|
112
130
|
import type { Router, Endpoint, Status, statusCodes } from "better-call";
|
|
113
131
|
|
|
114
132
|
interface CreateApiClientOptions {
|