@sanity/workflow-studio 0.7.1 → 0.9.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.
- package/CHANGELOG.md +60 -0
- package/README.md +57 -61
- package/dist/index.cjs +210 -133
- package/dist/index.d.cts +70 -52
- package/dist/index.d.ts +70 -52
- package/dist/index.js +212 -139
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,180 +1,112 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureDocumentExists, datasetResourceId, useKeyed } from "@sanity/workflow-react/observer";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { sdkInstancesStore, sdkGuardStore, sdkContentDocStore, sdkInstanceDocStore } from "@sanity/workflow-sdk/observer";
|
|
3
|
+
import { makeSdkObserver } from "@sanity/workflow-sdk/observer";
|
|
6
4
|
|
|
7
5
|
import { createSanityInstance } from "@sanity/sdk";
|
|
8
6
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import { useDocumentStore, useSource, useClient } from "sanity";
|
|
7
|
+
import { ENGINE_API_VERSION, createEngine, EXECUTION_KINDS } from "@sanity/workflow-engine";
|
|
12
8
|
|
|
13
|
-
import {
|
|
9
|
+
import { useMemo, useEffect, useState } from "react";
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
import { useSource, useClient } from "sanity";
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
let latest = initial;
|
|
19
|
-
return {
|
|
20
|
-
subscribe(onChange) {
|
|
21
|
-
let subscription, retry, stopped = !1;
|
|
22
|
-
const connect = () => {
|
|
23
|
-
subscription = observable.subscribe({
|
|
24
|
-
next: value => {
|
|
25
|
-
latest = resolve(value), onChange();
|
|
26
|
-
},
|
|
27
|
-
error: err => {
|
|
28
|
-
console.error(`[workflow-studio] ${name} stream errored — resubscribing in ${RESUBSCRIBE_DELAY_MS / 1e3}s:`, err),
|
|
29
|
-
stopped || (retry = setTimeout(connect, RESUBSCRIBE_DELAY_MS));
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
return connect(), () => {
|
|
34
|
-
stopped = !0, retry !== void 0 && clearTimeout(retry), subscription?.unsubscribe();
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
getSnapshot: () => latest
|
|
38
|
-
};
|
|
39
|
-
}
|
|
13
|
+
import { useWorkflowTelemetry, useWorkflowSession as useWorkflowSession$1, useDocumentWorkflows as useDocumentWorkflows$1, useWorkflowInstances as useWorkflowInstances$1 } from "@sanity/workflow-react";
|
|
40
14
|
|
|
41
|
-
function makeStudioObserver(
|
|
42
|
-
const {
|
|
43
|
-
observable: documentStore.pair.editState(id, type, release),
|
|
44
|
-
resolve: editState => resolveEditState(editState, draftFallback),
|
|
45
|
-
initial: void 0,
|
|
46
|
-
name: `the edit state of "${id}"`
|
|
47
|
-
}), requireSdk = what => {
|
|
48
|
-
if (sdk !== void 0) return sdk;
|
|
49
|
-
throw new Error(`@sanity/workflow-studio: ${what} — Studio's document store is bound to ${mounted.projectId}.${mounted.dataset} and cannot observe it. Pass an App SDK instance via the hook's \`sdk\` option (e.g. \`useWorkflowSession({engine, instanceId, sdk})\`) to observe foreign resources.`);
|
|
50
|
-
};
|
|
15
|
+
function makeStudioObserver(sdk, options) {
|
|
16
|
+
const {client: client, engineResource: engineResource} = options;
|
|
51
17
|
return {
|
|
52
|
-
|
|
53
|
-
id: instanceId,
|
|
54
|
-
type: WORKFLOW_INSTANCE_TYPE,
|
|
55
|
-
release: void 0,
|
|
56
|
-
draftFallback: !0
|
|
57
|
-
}) : sdkInstanceDocStore({
|
|
58
|
-
sdk: requireSdk(`instance "${instanceId}" lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
|
|
59
|
-
instanceId: instanceId,
|
|
18
|
+
...makeSdkObserver(sdk, {
|
|
60
19
|
engineResource: engineResource
|
|
61
20
|
}),
|
|
62
|
-
observeDocs: (documents, perspective) => combineDocStores(documents.map(ref => classifyRef(ref, mounted) === "mounted-dataset" ? {
|
|
63
|
-
key: ref.globalDocumentId,
|
|
64
|
-
store: editStateStore({
|
|
65
|
-
id: ref.documentId,
|
|
66
|
-
type: ref.type,
|
|
67
|
-
release: contentReleaseName({
|
|
68
|
-
ref: ref,
|
|
69
|
-
perspective: perspective
|
|
70
|
-
}),
|
|
71
|
-
draftFallback: contentDraftFallback({
|
|
72
|
-
ref: ref,
|
|
73
|
-
perspective: perspective
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
} : {
|
|
77
|
-
key: ref.globalDocumentId,
|
|
78
|
-
store: sdkContentDocStore({
|
|
79
|
-
sdk: requireSdk(`"${ref.globalDocumentId}" lives outside this workspace`),
|
|
80
|
-
ref: ref,
|
|
81
|
-
perspective: perspective
|
|
82
|
-
})
|
|
83
|
-
})),
|
|
84
|
-
observeGuards: (instanceId, resources) => combineGuardStores(resources.map(resource => {
|
|
85
|
-
if (sameDataset(resource, mounted)) {
|
|
86
|
-
const {query: query, params: params} = instanceGuardQuery(instanceId);
|
|
87
|
-
return observableStore({
|
|
88
|
-
observable: documentStore.listenQuery(query, params, {}),
|
|
89
|
-
resolve: docs => docs,
|
|
90
|
-
initial: void 0,
|
|
91
|
-
name: `the guard list for "${instanceId}"`
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return sdkGuardStore({
|
|
95
|
-
sdk: requireSdk(`guards for "${instanceId}" live in ${resource.projectId}.${resource.dataset}`),
|
|
96
|
-
instanceId: instanceId,
|
|
97
|
-
resource: resource
|
|
98
|
-
});
|
|
99
|
-
})),
|
|
100
|
-
observeInstances: query => engineIsMounted ? observableStore({
|
|
101
|
-
observable: documentStore.listenQuery(query.query, query.params, {}),
|
|
102
|
-
resolve: docs => docs,
|
|
103
|
-
initial: void 0,
|
|
104
|
-
name: "the instance list"
|
|
105
|
-
}) : sdkInstancesStore({
|
|
106
|
-
sdk: requireSdk(`the instance list lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
|
|
107
|
-
query: query,
|
|
108
|
-
resource: engineResource
|
|
109
|
-
}),
|
|
110
21
|
ensureDocumentExists: args => ensureDocumentExists(client, args)
|
|
111
22
|
};
|
|
112
23
|
}
|
|
113
24
|
|
|
114
|
-
function resolveEditState(editState, draftFallback) {
|
|
115
|
-
if (!editState.ready) return;
|
|
116
|
-
const draft = draftFallback ? editState.draft : null;
|
|
117
|
-
return editState.version ?? draft ?? editState.published;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
25
|
const SESSION_CLIENT_OPTIONS = {
|
|
121
26
|
apiVersion: ENGINE_API_VERSION
|
|
122
27
|
};
|
|
123
28
|
|
|
124
29
|
function useStudioObserver({engine: engine, sdk: suppliedSdk}) {
|
|
125
|
-
const
|
|
30
|
+
const source = useSource(), {projectId: projectId, dataset: dataset} = source, mounted = useMemo(() => ({
|
|
126
31
|
projectId: projectId,
|
|
127
32
|
dataset: dataset
|
|
128
|
-
}), [ projectId, dataset ]), engineResource = useMemo(() => datasetResourceId(engine.workflowResource), [ engine ]), sdk = useEmbeddedSdk({
|
|
33
|
+
}), [ projectId, dataset ]), engineResource = useMemo(() => datasetResourceId(engine.workflowResource), [ engine ]), studio = useStudioAuth(), sdk = useEmbeddedSdk({
|
|
129
34
|
supplied: suppliedSdk,
|
|
130
|
-
|
|
35
|
+
owner: source,
|
|
131
36
|
mounted: mounted,
|
|
132
|
-
|
|
37
|
+
studio: studio
|
|
133
38
|
});
|
|
134
|
-
return useMemo(() => makeStudioObserver(
|
|
135
|
-
mounted: mounted,
|
|
39
|
+
return useMemo(() => makeStudioObserver(sdk, {
|
|
136
40
|
client: source.getClient(SESSION_CLIENT_OPTIONS),
|
|
137
|
-
engineResource: engineResource
|
|
138
|
-
|
|
139
|
-
sdk: sdk
|
|
140
|
-
} : {}
|
|
141
|
-
}), [ documentStore, source, mounted, engineResource, sdk ]);
|
|
41
|
+
engineResource: engineResource
|
|
42
|
+
}), [ source, engineResource, sdk ]);
|
|
142
43
|
}
|
|
143
44
|
|
|
144
|
-
function
|
|
145
|
-
const
|
|
146
|
-
|
|
45
|
+
function useStudioAuth() {
|
|
46
|
+
const {authenticated: authenticated, auth: auth} = useSource();
|
|
47
|
+
return useMemo(() => {
|
|
48
|
+
const token = auth.token;
|
|
49
|
+
return token === void 0 ? {
|
|
50
|
+
authenticated: authenticated
|
|
51
|
+
} : {
|
|
52
|
+
authenticated: authenticated,
|
|
53
|
+
auth: {
|
|
54
|
+
token: token
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}, [ authenticated, auth.token ]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function useEmbeddedSdk({supplied: supplied, owner: owner, mounted: mounted, studio: studio}) {
|
|
61
|
+
const resolved = supplied === void 0 ? {
|
|
62
|
+
kind: "shared",
|
|
63
|
+
shared: sharedEmbeddedSdk({
|
|
64
|
+
owner: owner,
|
|
65
|
+
mounted: mounted,
|
|
66
|
+
studio: studio
|
|
67
|
+
})
|
|
68
|
+
} : {
|
|
69
|
+
kind: "supplied",
|
|
70
|
+
instance: supplied
|
|
71
|
+
};
|
|
72
|
+
return useSharedDispose(owner, resolved.kind === "shared" ? resolved.shared : void 0),
|
|
73
|
+
resolved.kind === "shared" ? resolved.shared.instance : resolved.instance;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sharedSdks = /* @__PURE__ */ new WeakMap;
|
|
77
|
+
|
|
78
|
+
function sharedEmbeddedSdk({owner: owner, mounted: mounted, studio: studio}) {
|
|
79
|
+
const key = `${mounted.projectId}.${mounted.dataset}`;
|
|
80
|
+
let byResource = sharedSdks.get(owner);
|
|
81
|
+
byResource === void 0 && (byResource = /* @__PURE__ */ new Map, sharedSdks.set(owner, byResource));
|
|
82
|
+
const current = byResource.get(key);
|
|
83
|
+
if (current !== void 0 && !current.instance.isDisposed()) return current;
|
|
84
|
+
const shared = {
|
|
85
|
+
instance: bootstrapInstance(mounted, studio),
|
|
147
86
|
key: key,
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return useDeferredDispose(embedded), supplied ?? embedded;
|
|
87
|
+
users: 0
|
|
88
|
+
};
|
|
89
|
+
return byResource.set(key, shared), shared;
|
|
152
90
|
}
|
|
153
91
|
|
|
154
|
-
function bootstrapInstance(mounted,
|
|
155
|
-
if (sessionToken === void 0) throw new Error("@sanity/workflow-studio: the engine's state dataset is not this workspace's dataset, and no Studio session token is readable to bootstrap an App SDK instance (cookie-authenticated studio?). Pass one explicitly via the hook's `sdk` option, e.g. useWorkflowSession({engine, instanceId, sdk}).");
|
|
92
|
+
function bootstrapInstance(mounted, studio) {
|
|
156
93
|
return createSanityInstance({
|
|
157
94
|
projectId: mounted.projectId,
|
|
158
95
|
dataset: mounted.dataset,
|
|
159
|
-
|
|
160
|
-
token: sessionToken
|
|
161
|
-
}
|
|
96
|
+
studio: studio
|
|
162
97
|
});
|
|
163
98
|
}
|
|
164
99
|
|
|
165
|
-
function
|
|
166
|
-
const disposal = useRef(void 0);
|
|
100
|
+
function useSharedDispose(owner, shared) {
|
|
167
101
|
useEffect(() => {
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
instance
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}, 0)
|
|
175
|
-
};
|
|
102
|
+
if (shared !== void 0) return shared.users += 1, () => {
|
|
103
|
+
const byResource = sharedSdks.get(owner);
|
|
104
|
+
byResource?.get(shared.key) === shared && (shared.users -= 1, setTimeout(() => {
|
|
105
|
+
shared.users !== 0 || byResource.get(shared.key) !== shared || (shared.instance.isDisposed() || shared.instance.dispose(),
|
|
106
|
+
byResource.delete(shared.key), byResource.size === 0 && sharedSdks.delete(owner));
|
|
107
|
+
}, 0));
|
|
176
108
|
};
|
|
177
|
-
}, [
|
|
109
|
+
}, [ owner, shared ]);
|
|
178
110
|
}
|
|
179
111
|
|
|
180
112
|
function useWorkflowEngine({workflowResource: workflowResource, tag: tag, resourceClients: resourceClients, effectHandlers: effectHandlers, missingHandler: missingHandler}) {
|
|
@@ -258,4 +190,145 @@ function useDocumentWorkflows({engine: engine, document: document, sdk: sdk}) {
|
|
|
258
190
|
});
|
|
259
191
|
}
|
|
260
192
|
|
|
261
|
-
|
|
193
|
+
const PROJECT_USERS_TTL_MS = 3e4;
|
|
194
|
+
|
|
195
|
+
function isRecord(value) {
|
|
196
|
+
return typeof value == "object" && value !== null && !Array.isArray(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRole(value) {
|
|
200
|
+
return isRecord(value) && typeof value.name == "string";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isMembership(value) {
|
|
204
|
+
return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isUser(value) {
|
|
208
|
+
return isRecord(value) && typeof value.id == "string" && (value.email === void 0 || typeof value.email == "string") && (value.displayName === void 0 || typeof value.displayName == "string") && (value.imageUrl === void 0 || value.imageUrl === null || typeof value.imageUrl == "string");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function fetchProjectUsers(client, projectId) {
|
|
212
|
+
const project = await client.request({
|
|
213
|
+
uri: `/projects/${projectId}`
|
|
214
|
+
});
|
|
215
|
+
if (!isRecord(project) || !Array.isArray(project.members)) throw new Error("Studio project user response did not contain a members array");
|
|
216
|
+
const membershipRows = project.members;
|
|
217
|
+
if (!membershipRows.every(isMembership)) throw new Error("Studio project user response contained an invalid membership row");
|
|
218
|
+
const memberships = membershipRows.filter(member => !member.isRobot);
|
|
219
|
+
if (memberships.length === 0) return [];
|
|
220
|
+
const ids = memberships.map(member => encodeURIComponent(member.id)), response = await client.request({
|
|
221
|
+
uri: `/projects/${projectId}/users/${ids.join(",")}`
|
|
222
|
+
}), profiles = (Array.isArray(response) ? response : [ response ]).filter(isUser), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
|
|
223
|
+
return memberships.map(membership => ({
|
|
224
|
+
membership: membership,
|
|
225
|
+
profile: profileById.get(membership.id)
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const projectUsersByClient = /* @__PURE__ */ new WeakMap;
|
|
230
|
+
|
|
231
|
+
function clientProjectUsers(client) {
|
|
232
|
+
const cached = projectUsersByClient.get(client);
|
|
233
|
+
if (cached) return cached;
|
|
234
|
+
const created = /* @__PURE__ */ new Map;
|
|
235
|
+
return projectUsersByClient.set(client, created), created;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function loadProjectUsers(client, projectId) {
|
|
239
|
+
const projectUsers = clientProjectUsers(client), cached = projectUsers.get(projectId);
|
|
240
|
+
if (cached && cached.expiresAt > Date.now()) return cached.promise;
|
|
241
|
+
projectUsers.delete(projectId);
|
|
242
|
+
const pending = fetchProjectUsers(client, projectId), entry = {
|
|
243
|
+
promise: pending,
|
|
244
|
+
expiresAt: Number.POSITIVE_INFINITY
|
|
245
|
+
};
|
|
246
|
+
return projectUsers.set(projectId, entry), pending.then(() => {
|
|
247
|
+
entry.expiresAt = Date.now() + PROJECT_USERS_TTL_MS;
|
|
248
|
+
}, () => {
|
|
249
|
+
projectUsers.get(projectId) === entry && projectUsers.delete(projectId);
|
|
250
|
+
}), pending;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function studioProjectUserDirectory(client, projectId) {
|
|
254
|
+
const load = () => loadProjectUsers(client, projectId);
|
|
255
|
+
return {
|
|
256
|
+
findById: async id => {
|
|
257
|
+
try {
|
|
258
|
+
const user = (await load()).find(candidate => candidate.membership.id === id);
|
|
259
|
+
return user ? {
|
|
260
|
+
status: "resolved",
|
|
261
|
+
user: user
|
|
262
|
+
} : {
|
|
263
|
+
status: "missing"
|
|
264
|
+
};
|
|
265
|
+
} catch (cause) {
|
|
266
|
+
return {
|
|
267
|
+
status: "inaccessible",
|
|
268
|
+
cause: cause
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
findByEmail: async email => {
|
|
273
|
+
try {
|
|
274
|
+
const normalized = email.toLowerCase(), user = (await load()).find(candidate => candidate.profile?.email?.toLowerCase() === normalized);
|
|
275
|
+
return user ? {
|
|
276
|
+
status: "resolved",
|
|
277
|
+
user: user
|
|
278
|
+
} : {
|
|
279
|
+
status: "missing"
|
|
280
|
+
};
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
return {
|
|
283
|
+
status: "inaccessible",
|
|
284
|
+
cause: cause
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function useStudioProjectUsers() {
|
|
292
|
+
const client = useClient({
|
|
293
|
+
apiVersion: ENGINE_API_VERSION
|
|
294
|
+
}), projectId = client.config().projectId, directory = useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), [state, setState] = useState({
|
|
295
|
+
users: [],
|
|
296
|
+
loading: projectId !== void 0,
|
|
297
|
+
error: projectId === void 0 ? new Error("No projectId on the Studio client") : void 0
|
|
298
|
+
});
|
|
299
|
+
return useEffect(() => {
|
|
300
|
+
if (!projectId) {
|
|
301
|
+
setState({
|
|
302
|
+
users: [],
|
|
303
|
+
loading: !1,
|
|
304
|
+
error: new Error("No projectId on the Studio client")
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let cancelled = !1;
|
|
309
|
+
return setState({
|
|
310
|
+
users: [],
|
|
311
|
+
loading: !0,
|
|
312
|
+
error: void 0
|
|
313
|
+
}), loadProjectUsers(client, projectId).then(users => {
|
|
314
|
+
cancelled || setState({
|
|
315
|
+
users: users,
|
|
316
|
+
loading: !1,
|
|
317
|
+
error: void 0
|
|
318
|
+
});
|
|
319
|
+
}, error => {
|
|
320
|
+
cancelled || setState({
|
|
321
|
+
users: [],
|
|
322
|
+
loading: !1,
|
|
323
|
+
error: error
|
|
324
|
+
});
|
|
325
|
+
}), () => {
|
|
326
|
+
cancelled = !0;
|
|
327
|
+
};
|
|
328
|
+
}, [ client, projectId ]), {
|
|
329
|
+
...state,
|
|
330
|
+
directory: directory
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export { makeStudioObserver, studioProjectUserDirectory, studioResourceClients, useDocumentWorkflows, useStudioObserver, useStudioProjectUsers, useWorkflowEngine, useWorkflowInstances, useWorkflowSession };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/workflow-studio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React adapter that drives the @sanity/workflow-engine reactive session
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "React adapter that drives the @sanity/workflow-engine reactive session in Sanity Studio through the App SDK store.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
7
7
|
"sanity-io",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@sanity/workflow-engine": "0.
|
|
45
|
-
"@sanity/workflow-react": "0.
|
|
46
|
-
"@sanity/workflow-sdk": "0.
|
|
44
|
+
"@sanity/workflow-engine": "0.18.0",
|
|
45
|
+
"@sanity/workflow-react": "0.13.0",
|
|
46
|
+
"@sanity/workflow-sdk": "0.10.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@sanity/pkg-utils": "^10.5.2",
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"jsdom": "^29.1.1",
|
|
54
54
|
"react": "^19.2.7",
|
|
55
55
|
"react-dom": "^19.2.7",
|
|
56
|
-
"sanity": "^
|
|
56
|
+
"sanity": "^6",
|
|
57
57
|
"vitest": "^4.1.8"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@sanity/sdk": "^2.12.0",
|
|
61
61
|
"react": "^19.2.7",
|
|
62
|
-
"sanity": "^
|
|
62
|
+
"sanity": "^6"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=20"
|