@createcms/core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { createClient } from 'better-call/client';
|
|
2
|
+
import { atom, onMount } from 'nanostores';
|
|
3
|
+
import 'better-call';
|
|
4
|
+
import { useRef, useCallback, useSyncExternalStore } from 'react';
|
|
5
|
+
export { BlocksRenderer, createBlocksMap, createBlocksRenderer, createContentRenderer, extractBlockEvents } from './blocks.js';
|
|
6
|
+
export { pickVariant } from './variant.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wire contract for the `withUser` / `withRoot` query flags, shared by the HTTP
|
|
10
|
+
* client (encode side, `client/proxy.ts`) and the server endpoint wrapper
|
|
11
|
+
* (decode side, `core/endpoint.ts`) so the two halves can never drift.
|
|
12
|
+
*
|
|
13
|
+
* Two transports carry these flags and both must round-trip:
|
|
14
|
+
* - **HTTP** — the proxy encodes values as strings (objects → JSON, booleans →
|
|
15
|
+
* `String()`); the endpoint decodes the strings back.
|
|
16
|
+
* - **In-process** (`cms.api.*`) — callers pass raw values (objects / real
|
|
17
|
+
* booleans) straight through, bypassing the proxy, so decode must accept the
|
|
18
|
+
* untransformed forms as well.
|
|
19
|
+
*
|
|
20
|
+
* This module is intentionally dependency-free (no drizzle, no better-call) so
|
|
21
|
+
* importing it from the client bundle stays cheap.
|
|
22
|
+
*/ const WITH_USER_KEY = 'withUser';
|
|
23
|
+
const WITH_ROOT_KEY = 'withRoot';
|
|
24
|
+
/**
|
|
25
|
+
* Encode the flag query for HTTP transport. Pure and copy-on-write: never
|
|
26
|
+
* mutates its input, and returns the *same* reference when nothing needed
|
|
27
|
+
* encoding (so an absent query stays absent, preserving GET-without-query).
|
|
28
|
+
*/ function encodeFlagQuery(query) {
|
|
29
|
+
if (!query) return query;
|
|
30
|
+
let out = query;
|
|
31
|
+
const rawWithUser = query[WITH_USER_KEY];
|
|
32
|
+
if (rawWithUser && typeof rawWithUser === 'object') {
|
|
33
|
+
out = {
|
|
34
|
+
...out,
|
|
35
|
+
[WITH_USER_KEY]: JSON.stringify(rawWithUser)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const rawWithRoot = query[WITH_ROOT_KEY];
|
|
39
|
+
if (rawWithRoot !== undefined) {
|
|
40
|
+
out = {
|
|
41
|
+
...out,
|
|
42
|
+
[WITH_ROOT_KEY]: String(rawWithRoot)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a Proxy-based client that maps property access to API calls.
|
|
50
|
+
*
|
|
51
|
+
* - `client.pages.listRoots()` -> `$fetch("/pages/listRoots", ...)`
|
|
52
|
+
* - Direct properties on `routes` (plugin actions, atom hooks, $fetch, $store)
|
|
53
|
+
* are returned as-is.
|
|
54
|
+
* - On successful API calls, matching `atomListeners` are triggered to
|
|
55
|
+
* invalidate dependent query atoms.
|
|
56
|
+
*/ function createDynamicPathProxy(routes, $fetch, pathMethods, atoms, atomListeners) {
|
|
57
|
+
return new Proxy(routes, {
|
|
58
|
+
get (target, prop) {
|
|
59
|
+
if (prop in target) {
|
|
60
|
+
const value = target[prop];
|
|
61
|
+
if (typeof value === 'object' && value !== null) {
|
|
62
|
+
return createNamespaceProxy(prop, value, $fetch, pathMethods, atoms, atomListeners);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
return createNamespaceProxy(prop, {}, $fetch, pathMethods, atoms, atomListeners);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function createNamespaceProxy(namespace, routes, $fetch, pathMethods, atoms, atomListeners) {
|
|
71
|
+
return new Proxy(routes, {
|
|
72
|
+
get (target, method) {
|
|
73
|
+
if (method in target) return target[method];
|
|
74
|
+
return (opts)=>{
|
|
75
|
+
const routePath = `/${namespace}/${method}`;
|
|
76
|
+
const httpMethod = pathMethods[routePath] ?? (opts?.body !== undefined ? 'POST' : 'GET');
|
|
77
|
+
const query = encodeFlagQuery(opts?.query);
|
|
78
|
+
return $fetch(routePath, {
|
|
79
|
+
method: httpMethod,
|
|
80
|
+
...opts,
|
|
81
|
+
...query ? {
|
|
82
|
+
query
|
|
83
|
+
} : {}
|
|
84
|
+
}).then((data)=>{
|
|
85
|
+
triggerListeners(routePath, atoms, atomListeners);
|
|
86
|
+
return data;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function triggerListeners(routePath, atoms, atomListeners) {
|
|
93
|
+
const matches = atomListeners.filter((l)=>l.matcher(routePath));
|
|
94
|
+
if (!matches.length) return;
|
|
95
|
+
const visited = new Set();
|
|
96
|
+
for (const match of matches){
|
|
97
|
+
const signal = atoms[match.signal];
|
|
98
|
+
if (!signal || visited.has(match.signal)) continue;
|
|
99
|
+
visited.add(match.signal);
|
|
100
|
+
// Defer to a microtask so the toggle runs after the current call settles,
|
|
101
|
+
// and read the CURRENT value at set-time (not a value captured earlier) so
|
|
102
|
+
// rapid successive mutations can't cancel each other out and drop an
|
|
103
|
+
// invalidation.
|
|
104
|
+
setTimeout(()=>{
|
|
105
|
+
const current = signal.get();
|
|
106
|
+
signal.set(typeof current === 'boolean' ? !current : current);
|
|
107
|
+
}, 0);
|
|
108
|
+
match.callback?.(routePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shared client assembly for both the vanilla and React entrypoints. The only
|
|
114
|
+
* thing that differs between them is how `media.useUploadAssets` is exposed:
|
|
115
|
+
* - vanilla passes the raw `uploadAssets` nanostores atom (consumers call
|
|
116
|
+
* `.get()` / `.subscribe()` themselves);
|
|
117
|
+
* - React passes a `() => useStore(uploadAssets)` hook thunk.
|
|
118
|
+
*
|
|
119
|
+
* That value is constructed by the caller and handed in as `unknown` so this
|
|
120
|
+
* module never imports React — keeping it out of the vanilla bundle. The
|
|
121
|
+
* `as CMSClientInstance` cast (present in both original builders) is the single
|
|
122
|
+
* intentional escape hatch that normalizes the loosely-typed routes object to
|
|
123
|
+
* the inferred public client type (`WithMedia` mandates
|
|
124
|
+
* `useUploadAssets: () => MediaUploadState`).
|
|
125
|
+
*/ function buildClient(config, useUploadAssets) {
|
|
126
|
+
const { $fetch, $store, pluginsActions, pluginsAtoms, pluginPathMethods, atomListeners, $ERROR_CODES } = config;
|
|
127
|
+
return createDynamicPathProxy({
|
|
128
|
+
media: {
|
|
129
|
+
useUploadAssets
|
|
130
|
+
},
|
|
131
|
+
...pluginsActions,
|
|
132
|
+
$fetch,
|
|
133
|
+
$store,
|
|
134
|
+
$ERROR_CODES
|
|
135
|
+
}, $fetch, pluginPathMethods, pluginsAtoms, atomListeners);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const CMS_ERRORS = {
|
|
139
|
+
BRANCH_NOT_FOUND: {
|
|
140
|
+
status: 404,
|
|
141
|
+
message: 'Branch not found'
|
|
142
|
+
},
|
|
143
|
+
BLOCK_NOT_FOUND: {
|
|
144
|
+
status: 404,
|
|
145
|
+
message: 'Block not found in snapshot'
|
|
146
|
+
},
|
|
147
|
+
PARENT_NOT_FOUND: {
|
|
148
|
+
status: 404,
|
|
149
|
+
message: 'Parent block not found'
|
|
150
|
+
},
|
|
151
|
+
ROOT_NOT_FOUND: {
|
|
152
|
+
status: 404,
|
|
153
|
+
message: 'Root block not found in snapshot'
|
|
154
|
+
},
|
|
155
|
+
ROOT_HAS_CHILDREN: {
|
|
156
|
+
status: 400,
|
|
157
|
+
message: 'Cannot delete a page that has child pages; archive or move the children first'
|
|
158
|
+
},
|
|
159
|
+
ROOT_IN_USE: {
|
|
160
|
+
status: 409,
|
|
161
|
+
message: 'Cannot delete: this root is embedded as a reusable block on live pages; remove those references first'
|
|
162
|
+
},
|
|
163
|
+
COMMIT_NOT_FOUND: {
|
|
164
|
+
status: 404,
|
|
165
|
+
message: 'Commit not found'
|
|
166
|
+
},
|
|
167
|
+
FOLDER_NOT_FOUND: {
|
|
168
|
+
status: 404,
|
|
169
|
+
message: 'Folder not found'
|
|
170
|
+
},
|
|
171
|
+
FOLDER_HAS_CONTENT: {
|
|
172
|
+
status: 400,
|
|
173
|
+
message: 'Cannot delete folder that contains assets or subfolders'
|
|
174
|
+
},
|
|
175
|
+
EMPTY_SNAPSHOT: {
|
|
176
|
+
status: 400,
|
|
177
|
+
message: 'Empty snapshot — no versions found'
|
|
178
|
+
},
|
|
179
|
+
BLOCK_ALREADY_DELETED: {
|
|
180
|
+
status: 400,
|
|
181
|
+
message: 'Block is already deleted'
|
|
182
|
+
},
|
|
183
|
+
TYPE_MISMATCH: {
|
|
184
|
+
status: 400,
|
|
185
|
+
message: 'Block type does not match the expected type'
|
|
186
|
+
},
|
|
187
|
+
USER_ID_REQUIRED: {
|
|
188
|
+
status: 400,
|
|
189
|
+
message: 'userId is required for this route when neither the request nor middleware provides one'
|
|
190
|
+
},
|
|
191
|
+
CANNOT_MOVE_ROOT: {
|
|
192
|
+
status: 400,
|
|
193
|
+
message: 'Cannot move the root block'
|
|
194
|
+
},
|
|
195
|
+
CANNOT_MOVE_INTO_SELF: {
|
|
196
|
+
status: 400,
|
|
197
|
+
message: 'Cannot move an item into itself'
|
|
198
|
+
},
|
|
199
|
+
CANNOT_MOVE_INTO_DESCENDANT: {
|
|
200
|
+
status: 400,
|
|
201
|
+
message: 'Cannot move an item into its own descendant'
|
|
202
|
+
},
|
|
203
|
+
MISSING_TARGET_PROPERTIES: {
|
|
204
|
+
status: 400,
|
|
205
|
+
message: 'targetProperties is required when duplicating a root'
|
|
206
|
+
},
|
|
207
|
+
BRANCH_NAME_ALREADY_EXISTS: {
|
|
208
|
+
status: 400,
|
|
209
|
+
message: 'A branch with this name already exists for this root'
|
|
210
|
+
},
|
|
211
|
+
CANNOT_RENAME_MAIN_BRANCH: {
|
|
212
|
+
status: 400,
|
|
213
|
+
message: 'The main branch cannot be renamed'
|
|
214
|
+
},
|
|
215
|
+
CANNOT_DELETE_MAIN_BRANCH: {
|
|
216
|
+
status: 400,
|
|
217
|
+
message: 'The main branch cannot be deleted'
|
|
218
|
+
},
|
|
219
|
+
BRANCH_HAS_PUBLICATIONS: {
|
|
220
|
+
status: 400,
|
|
221
|
+
message: 'Cannot delete a branch that has active publications'
|
|
222
|
+
},
|
|
223
|
+
BRANCH_HAS_OPEN_MERGE_REQUESTS: {
|
|
224
|
+
status: 400,
|
|
225
|
+
message: 'Cannot delete a branch that is part of open merge requests'
|
|
226
|
+
},
|
|
227
|
+
NO_COMMON_ANCESTOR: {
|
|
228
|
+
status: 400,
|
|
229
|
+
message: 'The two branches share no common ancestor'
|
|
230
|
+
},
|
|
231
|
+
MERGE_REQUEST_NOT_FOUND: {
|
|
232
|
+
status: 404,
|
|
233
|
+
message: 'Merge request not found'
|
|
234
|
+
},
|
|
235
|
+
MERGE_REQUEST_NOT_OPEN: {
|
|
236
|
+
status: 400,
|
|
237
|
+
message: 'Merge request is not open'
|
|
238
|
+
},
|
|
239
|
+
MERGE_REQUEST_NOT_CLOSED: {
|
|
240
|
+
status: 400,
|
|
241
|
+
message: 'Merge request is not closed'
|
|
242
|
+
},
|
|
243
|
+
MERGE_REQUEST_ALREADY_MERGED: {
|
|
244
|
+
status: 400,
|
|
245
|
+
message: 'Merge request has already been merged and cannot be reopened'
|
|
246
|
+
},
|
|
247
|
+
MERGE_REQUEST_ALREADY_EXISTS: {
|
|
248
|
+
status: 400,
|
|
249
|
+
message: 'An open merge request already exists for this source and target branch'
|
|
250
|
+
},
|
|
251
|
+
MERGE_REQUEST_OUTDATED: {
|
|
252
|
+
status: 400,
|
|
253
|
+
message: 'Merge request is outdated because the source branch changed after it was opened'
|
|
254
|
+
},
|
|
255
|
+
UNRESOLVED_CONFLICTS: {
|
|
256
|
+
status: 400,
|
|
257
|
+
message: 'Cannot merge: there are unresolved conflicts'
|
|
258
|
+
},
|
|
259
|
+
CONFLICT_NOT_FOUND: {
|
|
260
|
+
status: 404,
|
|
261
|
+
message: 'Merge conflict not found'
|
|
262
|
+
},
|
|
263
|
+
RESOLVED_VERSION_NOT_FOUND: {
|
|
264
|
+
status: 404,
|
|
265
|
+
message: 'The provided resolvedVersionId does not reference an existing block version'
|
|
266
|
+
},
|
|
267
|
+
APPROVAL_NOT_FOUND: {
|
|
268
|
+
status: 404,
|
|
269
|
+
message: 'Approval not found'
|
|
270
|
+
},
|
|
271
|
+
APPROVAL_ALREADY_REQUESTED: {
|
|
272
|
+
status: 400,
|
|
273
|
+
message: 'An approval has already been requested from this reviewer'
|
|
274
|
+
},
|
|
275
|
+
APPROVAL_NOT_PENDING: {
|
|
276
|
+
status: 400,
|
|
277
|
+
message: 'Approval is not pending'
|
|
278
|
+
},
|
|
279
|
+
APPROVAL_REVIEWER_MISMATCH: {
|
|
280
|
+
status: 403,
|
|
281
|
+
message: 'Only the requested reviewer can approve or reject this request'
|
|
282
|
+
},
|
|
283
|
+
APPROVAL_STALE: {
|
|
284
|
+
status: 400,
|
|
285
|
+
message: 'Approval is stale: the branch has advanced past the approved commit'
|
|
286
|
+
},
|
|
287
|
+
MERGE_APPROVAL_REQUIRED: {
|
|
288
|
+
status: 400,
|
|
289
|
+
message: 'Cannot merge: approval is required before execution'
|
|
290
|
+
},
|
|
291
|
+
PUBLICATION_APPROVAL_REQUIRED: {
|
|
292
|
+
status: 400,
|
|
293
|
+
message: 'Cannot publish: approval is required before publication'
|
|
294
|
+
},
|
|
295
|
+
APPROVALS_NOT_FULLY_APPROVED: {
|
|
296
|
+
status: 400,
|
|
297
|
+
message: 'Cannot proceed: not all requested approvals are approved'
|
|
298
|
+
},
|
|
299
|
+
BRANCHES_NOT_SAME_ROOT: {
|
|
300
|
+
status: 400,
|
|
301
|
+
message: 'Source and target branches must belong to the same root'
|
|
302
|
+
},
|
|
303
|
+
PUBLICATION_NOT_FOUND: {
|
|
304
|
+
status: 404,
|
|
305
|
+
message: 'Publication not found for this branch'
|
|
306
|
+
},
|
|
307
|
+
PUBLISHED_CONTENT_NOT_FOUND: {
|
|
308
|
+
status: 404,
|
|
309
|
+
message: 'No published content found'
|
|
310
|
+
},
|
|
311
|
+
AMBIGUOUS_SLUG: {
|
|
312
|
+
status: 400,
|
|
313
|
+
message: 'Multiple roots match this slug — use rootId for an unambiguous lookup'
|
|
314
|
+
},
|
|
315
|
+
DATA_RETENTION_NOT_CONFIGURED: {
|
|
316
|
+
status: 400,
|
|
317
|
+
message: 'dataRetention is not configured for this CMS instance'
|
|
318
|
+
},
|
|
319
|
+
MISSING_REQUIRED_S3_PARAMETERS: {
|
|
320
|
+
status: 400,
|
|
321
|
+
message: 'Missing required S3 parameters: hostname, accessKeyId, or secretAccessKey'
|
|
322
|
+
},
|
|
323
|
+
UNKNOWN_S3_PROVIDER: {
|
|
324
|
+
status: 400,
|
|
325
|
+
message: 'Unknown S3 provider specified'
|
|
326
|
+
},
|
|
327
|
+
SLUG_GENERATION_FAILED: {
|
|
328
|
+
status: 500,
|
|
329
|
+
message: 'Failed to generate a unique slug after maximum attempts'
|
|
330
|
+
},
|
|
331
|
+
TOO_MANY_FILES: {
|
|
332
|
+
status: 400,
|
|
333
|
+
message: 'Too many files in upload batch'
|
|
334
|
+
},
|
|
335
|
+
FILE_TOO_LARGE: {
|
|
336
|
+
status: 400,
|
|
337
|
+
message: 'One or more files exceed the maximum allowed size'
|
|
338
|
+
},
|
|
339
|
+
INVALID_FILE_TYPE: {
|
|
340
|
+
status: 400,
|
|
341
|
+
message: 'One or more files have a disallowed MIME type'
|
|
342
|
+
},
|
|
343
|
+
UPLOAD_FAILED: {
|
|
344
|
+
status: 500,
|
|
345
|
+
message: 'Server-side upload to S3 failed'
|
|
346
|
+
},
|
|
347
|
+
SLUG_ALREADY_EXISTS: {
|
|
348
|
+
status: 409,
|
|
349
|
+
message: 'A root with this slug on this collection with this parentRootId already exists'
|
|
350
|
+
},
|
|
351
|
+
SLUG_NOT_ENABLED: {
|
|
352
|
+
status: 400,
|
|
353
|
+
message: 'This collection does not have slugs enabled'
|
|
354
|
+
},
|
|
355
|
+
REDIRECT_NOT_FOUND: {
|
|
356
|
+
status: 404,
|
|
357
|
+
message: 'Redirect not found'
|
|
358
|
+
},
|
|
359
|
+
REDIRECT_INVALID: {
|
|
360
|
+
status: 400,
|
|
361
|
+
message: 'A redirect endpoint must be a page (rootId) or a path, matching its type'
|
|
362
|
+
},
|
|
363
|
+
REDIRECT_SOURCE_EXISTS: {
|
|
364
|
+
status: 409,
|
|
365
|
+
message: 'An active redirect already exists for this source'
|
|
366
|
+
},
|
|
367
|
+
SLUG_EMPTY_NOT_ALLOWED: {
|
|
368
|
+
status: 400,
|
|
369
|
+
message: 'Empty slug is not allowed for this collection (allowRoot is false)'
|
|
370
|
+
},
|
|
371
|
+
NESTING_NOT_ENABLED: {
|
|
372
|
+
status: 400,
|
|
373
|
+
message: 'parentRootId is not allowed — this collection does not have nested pages enabled'
|
|
374
|
+
},
|
|
375
|
+
CIRCULAR_REFERENCE: {
|
|
376
|
+
status: 400,
|
|
377
|
+
message: 'Cannot move a page under itself or one of its descendants'
|
|
378
|
+
},
|
|
379
|
+
PARENT_ROOT_NOT_FOUND: {
|
|
380
|
+
status: 404,
|
|
381
|
+
message: 'Parent root not found in this collection'
|
|
382
|
+
},
|
|
383
|
+
REFERENCE_DEPTH_EXCEEDED: {
|
|
384
|
+
status: 422,
|
|
385
|
+
message: 'Reference nesting is too deep (a reusable block embeds others past the limit)'
|
|
386
|
+
},
|
|
387
|
+
ASSET_NOT_FOUND: {
|
|
388
|
+
status: 404,
|
|
389
|
+
message: 'Asset not found'
|
|
390
|
+
},
|
|
391
|
+
VARIABLE_NOT_FOUND: {
|
|
392
|
+
status: 404,
|
|
393
|
+
message: 'Variable not found'
|
|
394
|
+
},
|
|
395
|
+
VARIABLE_KEY_EXISTS: {
|
|
396
|
+
status: 409,
|
|
397
|
+
message: 'A variable with this key already exists'
|
|
398
|
+
},
|
|
399
|
+
VARIABLE_IN_USE: {
|
|
400
|
+
status: 409,
|
|
401
|
+
message: 'Cannot delete variable: it is still in use'
|
|
402
|
+
},
|
|
403
|
+
TEMPLATE_NOT_FOUND: {
|
|
404
|
+
status: 404,
|
|
405
|
+
message: 'Template not found'
|
|
406
|
+
},
|
|
407
|
+
TEMPLATE_KEY_EXISTS: {
|
|
408
|
+
status: 409,
|
|
409
|
+
message: 'A template for this collection/block/property combination already exists'
|
|
410
|
+
},
|
|
411
|
+
ASSET_ACCESS_DENIED: {
|
|
412
|
+
status: 403,
|
|
413
|
+
message: 'This asset is private and requires authentication'
|
|
414
|
+
},
|
|
415
|
+
COMMENT_THREAD_NOT_FOUND: {
|
|
416
|
+
status: 404,
|
|
417
|
+
message: 'Comment thread not found'
|
|
418
|
+
},
|
|
419
|
+
COMMENT_THREAD_ALREADY_RESOLVED: {
|
|
420
|
+
status: 400,
|
|
421
|
+
message: 'Comment thread is already resolved'
|
|
422
|
+
},
|
|
423
|
+
COMMENT_THREAD_NOT_RESOLVED: {
|
|
424
|
+
status: 400,
|
|
425
|
+
message: 'Comment thread is not resolved'
|
|
426
|
+
},
|
|
427
|
+
COMMENT_MESSAGE_NOT_FOUND: {
|
|
428
|
+
status: 404,
|
|
429
|
+
message: 'Comment message not found'
|
|
430
|
+
},
|
|
431
|
+
COMMENT_MESSAGE_DELETED: {
|
|
432
|
+
status: 400,
|
|
433
|
+
message: 'Comment message has been deleted'
|
|
434
|
+
},
|
|
435
|
+
COMMENT_BODY_REQUIRED: {
|
|
436
|
+
status: 400,
|
|
437
|
+
message: 'Body is required for comment messages'
|
|
438
|
+
},
|
|
439
|
+
COMMENT_AUTHOR_MISMATCH: {
|
|
440
|
+
status: 403,
|
|
441
|
+
message: 'Only the author can edit or delete this message'
|
|
442
|
+
},
|
|
443
|
+
NOTIFICATION_NOT_FOUND: {
|
|
444
|
+
status: 404,
|
|
445
|
+
message: 'Notification not found'
|
|
446
|
+
},
|
|
447
|
+
NOTIFICATION_RECIPIENT_MISMATCH: {
|
|
448
|
+
status: 403,
|
|
449
|
+
message: 'You can only access your own notifications'
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Client-side CMS error thrown by `$fetch` when the server returns an error
|
|
455
|
+
* response. Unlike the server-side `CMSError` (which extends better-call's
|
|
456
|
+
* `APIError`), this is a plain `Error` subclass that works in the browser.
|
|
457
|
+
*/ class CMSClientError extends Error {
|
|
458
|
+
constructor(errorBody){
|
|
459
|
+
super(errorBody.message ?? errorBody.statusText ?? 'Unknown CMS error');
|
|
460
|
+
this.name = 'CMSClientError';
|
|
461
|
+
this.status = errorBody.status ?? 500;
|
|
462
|
+
this.statusText = errorBody.statusText ?? '';
|
|
463
|
+
this.code = errorBody.code;
|
|
464
|
+
}
|
|
465
|
+
get cmsCode() {
|
|
466
|
+
if (this.code && this.code in CMS_ERRORS) {
|
|
467
|
+
return this.code;
|
|
468
|
+
}
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// Constants
|
|
475
|
+
// ============================================================================
|
|
476
|
+
const FORBIDDEN_XHR_HEADERS = new Set([
|
|
477
|
+
'content-length',
|
|
478
|
+
'host',
|
|
479
|
+
'connection',
|
|
480
|
+
'user-agent',
|
|
481
|
+
'referer',
|
|
482
|
+
'origin'
|
|
483
|
+
]);
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// XHR Upload
|
|
486
|
+
// ============================================================================
|
|
487
|
+
function uploadWithXHR(url, file, headers, onProgress, signal) {
|
|
488
|
+
return new Promise((resolve, reject)=>{
|
|
489
|
+
if (signal?.aborted) {
|
|
490
|
+
reject(new DOMException('Upload aborted', 'AbortError'));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const xhr = new XMLHttpRequest();
|
|
494
|
+
xhr.open('PUT', url, true);
|
|
495
|
+
for (const [key, value] of Object.entries(headers)){
|
|
496
|
+
if (!FORBIDDEN_XHR_HEADERS.has(key.toLowerCase())) {
|
|
497
|
+
xhr.setRequestHeader(key, value);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (signal) {
|
|
501
|
+
signal.addEventListener('abort', ()=>{
|
|
502
|
+
xhr.abort();
|
|
503
|
+
reject(new DOMException('Upload aborted', 'AbortError'));
|
|
504
|
+
}, {
|
|
505
|
+
once: true
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
xhr.upload.onprogress = (event)=>{
|
|
509
|
+
if (event.lengthComputable) {
|
|
510
|
+
onProgress(event.loaded, event.total);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
xhr.onload = ()=>{
|
|
514
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
515
|
+
resolve();
|
|
516
|
+
} else {
|
|
517
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
xhr.onerror = ()=>reject(new Error('Network error during upload'));
|
|
521
|
+
xhr.onabort = ()=>reject(new DOMException('Upload aborted', 'AbortError'));
|
|
522
|
+
xhr.send(file);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// ============================================================================
|
|
526
|
+
// Helpers
|
|
527
|
+
// ============================================================================
|
|
528
|
+
function computeTotalProgress(files) {
|
|
529
|
+
if (files.length === 0) return 0;
|
|
530
|
+
const sum = files.reduce((acc, f)=>acc + f.progress, 0);
|
|
531
|
+
return Math.round(sum / files.length);
|
|
532
|
+
}
|
|
533
|
+
// ============================================================================
|
|
534
|
+
// Atom Factory
|
|
535
|
+
// ============================================================================
|
|
536
|
+
const INITIAL_STATE = {
|
|
537
|
+
isUploading: false,
|
|
538
|
+
isAborted: false,
|
|
539
|
+
files: [],
|
|
540
|
+
totalProgress: 0,
|
|
541
|
+
error: null,
|
|
542
|
+
upload: ()=>Promise.resolve(),
|
|
543
|
+
abort: ()=>{},
|
|
544
|
+
reset: ()=>{}
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Creates a nanostores atom that manages media upload state.
|
|
548
|
+
*
|
|
549
|
+
* Pipeline: sign -> upload primary files.
|
|
550
|
+
*
|
|
551
|
+
* Server-side validation (file size, count, MIME types) is handled by the
|
|
552
|
+
* `createSignedUpload` endpoint.
|
|
553
|
+
*/ function createMediaUploadAtom($fetch) {
|
|
554
|
+
const store = atom({
|
|
555
|
+
...INITIAL_STATE
|
|
556
|
+
});
|
|
557
|
+
let controller = null;
|
|
558
|
+
function updateFileState(index, patch) {
|
|
559
|
+
const current = store.get();
|
|
560
|
+
const files = [
|
|
561
|
+
...current.files
|
|
562
|
+
];
|
|
563
|
+
files[index] = {
|
|
564
|
+
...files[index],
|
|
565
|
+
...patch
|
|
566
|
+
};
|
|
567
|
+
store.set({
|
|
568
|
+
...current,
|
|
569
|
+
files,
|
|
570
|
+
totalProgress: computeTotalProgress(files)
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function abort() {
|
|
574
|
+
controller?.abort();
|
|
575
|
+
controller = null;
|
|
576
|
+
const current = store.get();
|
|
577
|
+
store.set({
|
|
578
|
+
...current,
|
|
579
|
+
isUploading: false,
|
|
580
|
+
isAborted: true
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function reset() {
|
|
584
|
+
controller?.abort();
|
|
585
|
+
controller = null;
|
|
586
|
+
store.set({
|
|
587
|
+
...INITIAL_STATE,
|
|
588
|
+
upload,
|
|
589
|
+
abort,
|
|
590
|
+
reset
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
async function upload(files, options) {
|
|
594
|
+
if (files.length === 0) return;
|
|
595
|
+
controller = new AbortController();
|
|
596
|
+
const { signal } = controller;
|
|
597
|
+
store.set({
|
|
598
|
+
...store.get(),
|
|
599
|
+
isUploading: true,
|
|
600
|
+
isAborted: false,
|
|
601
|
+
files: files.map((f)=>({
|
|
602
|
+
name: f.name,
|
|
603
|
+
progress: 0,
|
|
604
|
+
status: 'pending'
|
|
605
|
+
})),
|
|
606
|
+
totalProgress: 0,
|
|
607
|
+
error: null
|
|
608
|
+
});
|
|
609
|
+
try {
|
|
610
|
+
if (signal.aborted) return;
|
|
611
|
+
// 1. Sign files
|
|
612
|
+
const signResponse = await $fetch('/media/createSignedUpload', {
|
|
613
|
+
method: 'POST',
|
|
614
|
+
body: {
|
|
615
|
+
files: files.map((f)=>({
|
|
616
|
+
name: f.name,
|
|
617
|
+
size: f.size,
|
|
618
|
+
type: f.type
|
|
619
|
+
})),
|
|
620
|
+
folderId: options?.folderId
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
if (signal.aborted) return;
|
|
624
|
+
// 2. Upload files to S3
|
|
625
|
+
const uploadPromises = signResponse.assets.map(async (asset, index)=>{
|
|
626
|
+
updateFileState(index, {
|
|
627
|
+
status: 'uploading'
|
|
628
|
+
});
|
|
629
|
+
try {
|
|
630
|
+
await uploadWithXHR(asset.signedUrl, files[index], asset.headers, (loaded, total)=>{
|
|
631
|
+
updateFileState(index, {
|
|
632
|
+
progress: Math.round(loaded / total * 100)
|
|
633
|
+
});
|
|
634
|
+
}, signal);
|
|
635
|
+
updateFileState(index, {
|
|
636
|
+
progress: 100,
|
|
637
|
+
status: 'done',
|
|
638
|
+
result: {
|
|
639
|
+
id: asset.id,
|
|
640
|
+
slug: asset.slug,
|
|
641
|
+
objectKey: asset.objectKey
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
} catch (err) {
|
|
645
|
+
const isAbortErr = err instanceof DOMException && err.name === 'AbortError';
|
|
646
|
+
updateFileState(index, {
|
|
647
|
+
status: 'error',
|
|
648
|
+
error: isAbortErr ? 'Upload aborted.' : err instanceof Error ? err.message : 'Upload failed'
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
await Promise.allSettled(uploadPromises);
|
|
653
|
+
// 3. Finalize
|
|
654
|
+
const finalState = store.get();
|
|
655
|
+
const hasErrors = finalState.files.some((f)=>f.status === 'error');
|
|
656
|
+
store.set({
|
|
657
|
+
...finalState,
|
|
658
|
+
isUploading: false,
|
|
659
|
+
totalProgress: computeTotalProgress(finalState.files),
|
|
660
|
+
error: hasErrors ? 'Some files failed to upload' : null
|
|
661
|
+
});
|
|
662
|
+
} catch (err) {
|
|
663
|
+
store.set({
|
|
664
|
+
...store.get(),
|
|
665
|
+
isUploading: false,
|
|
666
|
+
error: err instanceof Error ? err.message : err
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
store.set({
|
|
671
|
+
...INITIAL_STATE,
|
|
672
|
+
upload,
|
|
673
|
+
abort,
|
|
674
|
+
reset
|
|
675
|
+
});
|
|
676
|
+
return store;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Creates a `CMSClientStore` from a map of atoms.
|
|
681
|
+
* Provides `notify` (toggle a signal) and `listen` (subscribe to a signal).
|
|
682
|
+
*/ function createStore(atoms) {
|
|
683
|
+
return {
|
|
684
|
+
notify (signal) {
|
|
685
|
+
const atom = atoms[signal];
|
|
686
|
+
if (atom) {
|
|
687
|
+
const current = atom.get();
|
|
688
|
+
atom.set(typeof current === 'boolean' ? !current : current);
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
listen (signal, listener) {
|
|
692
|
+
const atom = atoms[signal];
|
|
693
|
+
if (atom) atom.subscribe(listener);
|
|
694
|
+
},
|
|
695
|
+
atoms
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Builds the client config synchronously. Atoms, actions, listeners and
|
|
701
|
+
* error codes are available immediately so React hooks work from the
|
|
702
|
+
* first render. Plugin `init` (async) is NOT called here — use
|
|
703
|
+
* `runPluginInit` afterwards.
|
|
704
|
+
*/ function getClientConfigSync(options) {
|
|
705
|
+
const plugins = options.plugins ?? [];
|
|
706
|
+
const betterCallClient = createClient({
|
|
707
|
+
baseURL: options.baseURL
|
|
708
|
+
});
|
|
709
|
+
const $fetch = async (path, opts)=>{
|
|
710
|
+
const res = await betterCallClient(path, opts);
|
|
711
|
+
if (res.error) throw new CMSClientError(res.error);
|
|
712
|
+
return res.data;
|
|
713
|
+
};
|
|
714
|
+
const pluginsAtoms = {
|
|
715
|
+
$mediaSignal: atom(false),
|
|
716
|
+
uploadAssets: createMediaUploadAtom($fetch)
|
|
717
|
+
};
|
|
718
|
+
const pluginPathMethods = {};
|
|
719
|
+
const atomListeners = [
|
|
720
|
+
{
|
|
721
|
+
matcher: (path)=>path.startsWith('/media/'),
|
|
722
|
+
signal: '$mediaSignal'
|
|
723
|
+
}
|
|
724
|
+
];
|
|
725
|
+
const $store = createStore(pluginsAtoms);
|
|
726
|
+
for (const plugin of plugins){
|
|
727
|
+
if (plugin.pathMethods) {
|
|
728
|
+
Object.assign(pluginPathMethods, plugin.pathMethods);
|
|
729
|
+
}
|
|
730
|
+
if (plugin.atomListeners) {
|
|
731
|
+
atomListeners.push(...plugin.atomListeners);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
let pluginsActions = {};
|
|
735
|
+
for (const plugin of plugins){
|
|
736
|
+
if (plugin.getActions) {
|
|
737
|
+
pluginsActions = {
|
|
738
|
+
...pluginsActions,
|
|
739
|
+
...plugin.getActions($fetch, $store, options.baseURL)
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const $ERROR_CODES = {};
|
|
744
|
+
for (const plugin of plugins){
|
|
745
|
+
if (plugin.$ERROR_CODES) {
|
|
746
|
+
Object.assign($ERROR_CODES, plugin.$ERROR_CODES);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
$fetch,
|
|
751
|
+
$store,
|
|
752
|
+
pluginsActions,
|
|
753
|
+
pluginsAtoms,
|
|
754
|
+
pluginPathMethods,
|
|
755
|
+
atomListeners,
|
|
756
|
+
$ERROR_CODES
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Runs async plugin `init` functions sequentially. Call this after
|
|
761
|
+
* `getClientConfigSync` — the config is already usable before init
|
|
762
|
+
* completes, so React hooks work immediately.
|
|
763
|
+
*/ async function runPluginInit(options, config) {
|
|
764
|
+
const plugins = options.plugins ?? [];
|
|
765
|
+
for (const plugin of plugins){
|
|
766
|
+
// init runs for its side effects. (Client plugins surface state via
|
|
767
|
+
// getActions/atoms, which already close over their own config — there is
|
|
768
|
+
// no shared client context to populate, so any returned `context` is
|
|
769
|
+
// intentionally not collected.)
|
|
770
|
+
await plugin.init?.(config.$fetch, config.$store);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* React hook that subscribes to a nanostores atom via `useSyncExternalStore`.
|
|
776
|
+
* Re-renders the component when the atom value changes.
|
|
777
|
+
*/ function useStore(store) {
|
|
778
|
+
const snapshotRef = useRef(store.get());
|
|
779
|
+
const subscribe = useCallback((onChange)=>{
|
|
780
|
+
const emitChange = (value)=>{
|
|
781
|
+
if (snapshotRef.current === value) return;
|
|
782
|
+
snapshotRef.current = value;
|
|
783
|
+
onChange();
|
|
784
|
+
};
|
|
785
|
+
emitChange(store.get());
|
|
786
|
+
return store.listen(emitChange);
|
|
787
|
+
}, [
|
|
788
|
+
store
|
|
789
|
+
]);
|
|
790
|
+
const getSnapshot = ()=>snapshotRef.current;
|
|
791
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function createCMSClient(options) {
|
|
795
|
+
if (options) {
|
|
796
|
+
const config = getClientConfigSync(options);
|
|
797
|
+
runPluginInit(options, config);
|
|
798
|
+
return buildClient(config, ()=>useStore(config.pluginsAtoms.uploadAssets));
|
|
799
|
+
}
|
|
800
|
+
return (opts)=>{
|
|
801
|
+
const config = getClientConfigSync(opts);
|
|
802
|
+
runPluginInit(opts, config);
|
|
803
|
+
return buildClient(config, ()=>useStore(config.pluginsAtoms.uploadAssets));
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const isServer = ()=>typeof window === 'undefined';
|
|
808
|
+
/**
|
|
809
|
+
* Creates a reactive query atom that fetches data from a CMS endpoint.
|
|
810
|
+
* Subscribes to one or more signal atoms and refetches when they toggle.
|
|
811
|
+
*
|
|
812
|
+
* Returns a nanostores `WritableAtom` — framework-agnostic, not a React hook.
|
|
813
|
+
*/ function createCMSQuery(signals, path, $fetch, options) {
|
|
814
|
+
const value = atom({
|
|
815
|
+
data: null,
|
|
816
|
+
error: null,
|
|
817
|
+
isPending: true,
|
|
818
|
+
isRefetching: false,
|
|
819
|
+
refetch: ()=>fetchData()
|
|
820
|
+
});
|
|
821
|
+
const fetchData = async ()=>{
|
|
822
|
+
const current = value.get();
|
|
823
|
+
value.set({
|
|
824
|
+
...current,
|
|
825
|
+
isPending: current.data === null,
|
|
826
|
+
isRefetching: current.data !== null,
|
|
827
|
+
error: null
|
|
828
|
+
});
|
|
829
|
+
try {
|
|
830
|
+
const opts = typeof options === 'function' ? options() : options;
|
|
831
|
+
const data = await $fetch(path, {
|
|
832
|
+
method: opts?.method ?? 'GET',
|
|
833
|
+
query: opts?.query
|
|
834
|
+
});
|
|
835
|
+
value.set({
|
|
836
|
+
data,
|
|
837
|
+
error: null,
|
|
838
|
+
isPending: false,
|
|
839
|
+
isRefetching: false,
|
|
840
|
+
refetch: ()=>fetchData()
|
|
841
|
+
});
|
|
842
|
+
} catch (error) {
|
|
843
|
+
value.set({
|
|
844
|
+
...value.get(),
|
|
845
|
+
error,
|
|
846
|
+
isPending: false,
|
|
847
|
+
isRefetching: false
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const signalList = Array.isArray(signals) ? signals : [
|
|
852
|
+
signals
|
|
853
|
+
];
|
|
854
|
+
// Activate only while the query atom has subscribers. On mount, do an initial
|
|
855
|
+
// fetch and listen to each signal for changes; on unmount, call the
|
|
856
|
+
// per-listener unsubscribers. We deliberately use the unsubscribe functions
|
|
857
|
+
// returned by `listen` rather than `atom.off()` — `off()` removes EVERY
|
|
858
|
+
// listener on the (often shared) signal atom, which would break other queries
|
|
859
|
+
// subscribed to the same signal.
|
|
860
|
+
onMount(value, ()=>{
|
|
861
|
+
if (!isServer()) void fetchData();
|
|
862
|
+
const unsubscribers = signalList.map((signal)=>signal.listen(()=>{
|
|
863
|
+
if (!isServer()) void fetchData();
|
|
864
|
+
}));
|
|
865
|
+
return ()=>{
|
|
866
|
+
for (const unsubscribe of unsubscribers)unsubscribe();
|
|
867
|
+
};
|
|
868
|
+
});
|
|
869
|
+
return value;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export { CMSClientError, createCMSClient, createCMSQuery, useStore };
|