@sanity/sdk 2.8.0 → 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.
- package/dist/_chunks-dts/utils.d.ts +2450 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +465 -1813
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +18 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +44 -30
- package/src/client/clientStore.ts +49 -48
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +78 -12
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +542 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +106 -78
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +47 -40
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +96 -0
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +33 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +12 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +2 -3
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/reducers.ts +3 -4
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {
|
|
3
|
+
type ConsentStatus,
|
|
4
|
+
createBatchedStore,
|
|
5
|
+
type SessionId,
|
|
6
|
+
type TelemetryEvent,
|
|
7
|
+
type TelemetryLogger,
|
|
8
|
+
type TelemetryStore,
|
|
9
|
+
} from '@sanity/telemetry'
|
|
10
|
+
|
|
11
|
+
import {createLogger} from '../utils/logger'
|
|
12
|
+
import {CORE_SDK_VERSION} from '../version'
|
|
13
|
+
import {
|
|
14
|
+
SDKDevError,
|
|
15
|
+
SDKDevSessionEnded,
|
|
16
|
+
SDKDevSessionStarted,
|
|
17
|
+
SDKHookMounted,
|
|
18
|
+
} from './__telemetry__/sdk.telemetry'
|
|
19
|
+
|
|
20
|
+
const FLUSH_INTERVAL_MS = 30_000
|
|
21
|
+
const CONSENT_TAG = 'telemetry-consent.sdk'
|
|
22
|
+
const BATCH_TAG = 'telemetry.batch'
|
|
23
|
+
|
|
24
|
+
const log = createLogger('telemetry')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Manages dev-mode telemetry for a single SDK instance.
|
|
28
|
+
*
|
|
29
|
+
* Wraps `@sanity/telemetry`'s batched store with SDK-specific concerns:
|
|
30
|
+
* consent caching, session lifecycle events, and hook usage tracking.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export interface TelemetryManager {
|
|
35
|
+
/**
|
|
36
|
+
* Eagerly resolve and cache the user's consent status.
|
|
37
|
+
* Returns true only when the user has explicitly opted in (`granted`).
|
|
38
|
+
* Call this before logging any events to avoid buffering events that
|
|
39
|
+
* will be dropped on the first flush.
|
|
40
|
+
*/
|
|
41
|
+
checkConsent(): Promise<boolean>
|
|
42
|
+
|
|
43
|
+
/** Log a "SDK Dev Session Started" event */
|
|
44
|
+
logSessionStarted(data: {projectId: string; perspective: string; authMethod: string}): void
|
|
45
|
+
|
|
46
|
+
/** Log a "SDK Hook First Used" event (deduplicated per hook name) */
|
|
47
|
+
logHookFirstUsed(hookName: string): void
|
|
48
|
+
|
|
49
|
+
/** Log a "SDK Dev Error" event */
|
|
50
|
+
logDevError(errorType: string, hookName: string): void
|
|
51
|
+
|
|
52
|
+
/** Log a "SDK Dev Session Ended" event and tear down the store */
|
|
53
|
+
endSession(): void
|
|
54
|
+
|
|
55
|
+
/** Tear down the store without logging a session-end event */
|
|
56
|
+
dispose(): void
|
|
57
|
+
|
|
58
|
+
/** The set of hook names used during this session */
|
|
59
|
+
readonly hooksUsed: ReadonlySet<string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface TelemetryManagerOptions {
|
|
63
|
+
sessionId: string
|
|
64
|
+
getClient: () => SanityClient
|
|
65
|
+
projectId: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates a telemetry manager for a single SDK instance session.
|
|
70
|
+
*
|
|
71
|
+
* The manager initializes a `createBatchedStore` from `@sanity/telemetry`,
|
|
72
|
+
* caches the consent check for the lifetime of the session, and provides
|
|
73
|
+
* typed methods for each SDK telemetry event.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export function createTelemetryManager(options: TelemetryManagerOptions): TelemetryManager {
|
|
78
|
+
const {sessionId, getClient, projectId} = options
|
|
79
|
+
const startedAt = Date.now()
|
|
80
|
+
const emittedHooks = new Set<string>()
|
|
81
|
+
|
|
82
|
+
let cachedConsent: {status: ConsentStatus} | null = null
|
|
83
|
+
|
|
84
|
+
const resolveConsent = async (): Promise<{status: ConsentStatus}> => {
|
|
85
|
+
if (cachedConsent) return cachedConsent
|
|
86
|
+
try {
|
|
87
|
+
const client = getClient()
|
|
88
|
+
const result = await client.request<{status: ConsentStatus}>({
|
|
89
|
+
uri: '/intake/telemetry-status',
|
|
90
|
+
tag: CONSENT_TAG,
|
|
91
|
+
})
|
|
92
|
+
cachedConsent = result
|
|
93
|
+
} catch {
|
|
94
|
+
cachedConsent = {status: 'undetermined'}
|
|
95
|
+
}
|
|
96
|
+
return cachedConsent
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const enrichBatch = (batch: TelemetryEvent[]) =>
|
|
100
|
+
batch.map((event) => ({
|
|
101
|
+
...event,
|
|
102
|
+
context: {
|
|
103
|
+
version: CORE_SDK_VERSION,
|
|
104
|
+
environment: 'development' as const,
|
|
105
|
+
origin: typeof window !== 'undefined' ? window.location.origin : 'node',
|
|
106
|
+
},
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
const sendEvents = async (batch: TelemetryEvent[]): Promise<unknown> => {
|
|
110
|
+
const client = getClient()
|
|
111
|
+
log.debug('sending event batch', {batchSize: batch.length})
|
|
112
|
+
return client.request({
|
|
113
|
+
uri: '/intake/batch',
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: {projectId, batch: enrichBatch(batch)},
|
|
116
|
+
tag: BATCH_TAG,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const store: TelemetryStore<Record<string, unknown>> = createBatchedStore(
|
|
121
|
+
sessionId as SessionId,
|
|
122
|
+
{
|
|
123
|
+
flushInterval: FLUSH_INTERVAL_MS,
|
|
124
|
+
resolveConsent,
|
|
125
|
+
sendEvents,
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const logger: TelemetryLogger<Record<string, unknown>> = store.logger
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
async checkConsent() {
|
|
133
|
+
const {status} = await resolveConsent()
|
|
134
|
+
return status === 'granted'
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
logSessionStarted(data) {
|
|
138
|
+
log.debug('event: SDK Dev Session Started', {
|
|
139
|
+
projectId: data.projectId,
|
|
140
|
+
perspective: data.perspective,
|
|
141
|
+
authMethod: data.authMethod,
|
|
142
|
+
version: CORE_SDK_VERSION,
|
|
143
|
+
})
|
|
144
|
+
logger.log(SDKDevSessionStarted, {
|
|
145
|
+
version: CORE_SDK_VERSION,
|
|
146
|
+
...data,
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
logHookFirstUsed(hookName: string) {
|
|
151
|
+
if (emittedHooks.has(hookName)) return
|
|
152
|
+
emittedHooks.add(hookName)
|
|
153
|
+
log.debug('event: SDK Hook Mounted', {hookName})
|
|
154
|
+
logger.log(SDKHookMounted, {hookName})
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
logDevError(errorType: string, hookName: string) {
|
|
158
|
+
log.debug('event: SDK Dev Error', {errorType, hookName})
|
|
159
|
+
logger.log(SDKDevError, {errorType, hookName})
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
endSession() {
|
|
163
|
+
const durationSeconds = Math.round((Date.now() - startedAt) / 1000)
|
|
164
|
+
log.debug('event: SDK Dev Session Ended', {
|
|
165
|
+
durationSeconds,
|
|
166
|
+
hooksUsed: [...emittedHooks],
|
|
167
|
+
})
|
|
168
|
+
logger.log(SDKDevSessionEnded, {
|
|
169
|
+
durationSeconds,
|
|
170
|
+
hooksUsed: [...emittedHooks],
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
store.flush().catch(() => {
|
|
174
|
+
// Best-effort flush on dispose; swallow errors
|
|
175
|
+
})
|
|
176
|
+
store.end()
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
dispose() {
|
|
180
|
+
store.end()
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
get hooksUsed(): ReadonlySet<string> {
|
|
184
|
+
return emittedHooks
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/users/reducers.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {omit} from 'lodash-es'
|
|
2
|
-
|
|
3
1
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
2
|
+
import {omitProperty} from '../utils/object'
|
|
4
3
|
import {type GetUsersOptions, type SanityUserResponse, type UsersStoreState} from './types'
|
|
5
4
|
import {DEFAULT_USERS_BATCH_SIZE} from './usersConstants'
|
|
6
5
|
|
|
@@ -48,7 +47,7 @@ export const removeSubscription =
|
|
|
48
47
|
const group = prev.users[key]
|
|
49
48
|
if (!group) return prev
|
|
50
49
|
const subscriptions = group.subscriptions.filter((id) => id !== subscriptionId)
|
|
51
|
-
if (!subscriptions.length) return {...prev, users:
|
|
50
|
+
if (!subscriptions.length) return {...prev, users: omitProperty(prev.users, key)}
|
|
52
51
|
return {...prev, users: {...prev.users, [key]: {...group, subscriptions}}}
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -83,7 +82,7 @@ export const cancelRequest =
|
|
|
83
82
|
const group = prev.users[key]
|
|
84
83
|
if (!group) return prev
|
|
85
84
|
if (group.subscriptions.length) return prev
|
|
86
|
-
return {...prev, users:
|
|
85
|
+
return {...prev, users: omitProperty(prev.users, key)}
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
export const initializeRequest =
|
package/src/users/usersStore.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {createStateSourceAction, type SelectorContext} from '../store/createStat
|
|
|
31
31
|
import {type StoreState} from '../store/createStoreState'
|
|
32
32
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
33
33
|
import {insecureRandomId} from '../utils/ids'
|
|
34
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
34
35
|
import {
|
|
35
36
|
addSubscription,
|
|
36
37
|
cancelRequest,
|
|
@@ -130,6 +131,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
130
131
|
.request<PatchedSanityUserFromClient>({
|
|
131
132
|
method: 'GET',
|
|
132
133
|
uri: `/users/${userId}`,
|
|
134
|
+
tag: 'users.get',
|
|
133
135
|
})
|
|
134
136
|
.pipe(
|
|
135
137
|
map((user) => {
|
|
@@ -183,6 +185,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
183
185
|
.request<SanityUser | SanityUserResponse>({
|
|
184
186
|
method: 'GET',
|
|
185
187
|
uri: `access/${resourceType}/${resourceId}/users/${userId}`,
|
|
188
|
+
tag: 'users.get',
|
|
186
189
|
})
|
|
187
190
|
.pipe(
|
|
188
191
|
map((response) => {
|
|
@@ -251,6 +254,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
251
254
|
client.observable.request<SanityUserResponse>({
|
|
252
255
|
method: 'GET',
|
|
253
256
|
uri: `access/${resource.type}/${resource.id}/users`,
|
|
257
|
+
tag: 'users.list',
|
|
254
258
|
query: cursor
|
|
255
259
|
? {nextCursor: cursor, limit: batchSize.toString()}
|
|
256
260
|
: {limit: batchSize.toString()},
|
|
@@ -310,7 +314,7 @@ export const getUsersState = bindActionGlobally(
|
|
|
310
314
|
const key = getUsersKey(instance, options)
|
|
311
315
|
state.set('addSubscription', addSubscription(subscriptionId, key))
|
|
312
316
|
return () => {
|
|
313
|
-
|
|
317
|
+
setCleanupTimeout(
|
|
314
318
|
() => state.set('removeSubscription', removeSubscription(subscriptionId, key)),
|
|
315
319
|
USERS_STATE_CLEAR_DELAY,
|
|
316
320
|
)
|
|
@@ -127,8 +127,9 @@ describe('createFetcherStore', () => {
|
|
|
127
127
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
128
128
|
|
|
129
129
|
// Second subscription within throttle interval
|
|
130
|
-
const
|
|
131
|
-
|
|
130
|
+
const stateSource2 = store.getState(instance, 1)
|
|
131
|
+
const sub2 = stateSource2.subscribe()
|
|
132
|
+
await firstValueFrom(stateSource2.observable)
|
|
132
133
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
133
134
|
|
|
134
135
|
// Advance past throttle interval
|
|
@@ -136,8 +137,9 @@ describe('createFetcherStore', () => {
|
|
|
136
137
|
await vi.advanceTimersByTimeAsync(1000)
|
|
137
138
|
|
|
138
139
|
// Third subscription after throttle interval
|
|
139
|
-
const
|
|
140
|
-
|
|
140
|
+
const stateSource3 = store.getState(instance, 1)
|
|
141
|
+
const sub3 = stateSource3.subscribe()
|
|
142
|
+
await firstValueFrom(stateSource3.observable)
|
|
141
143
|
expect(fetchSpy).toHaveBeenCalledTimes(2)
|
|
142
144
|
|
|
143
145
|
sub1()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {omit} from 'lodash-es'
|
|
2
1
|
import {asapScheduler, EMPTY, firstValueFrom, from, Observable} from 'rxjs'
|
|
3
2
|
import {
|
|
4
3
|
catchError,
|
|
@@ -23,6 +22,8 @@ import {
|
|
|
23
22
|
} from '../store/createStateSourceAction'
|
|
24
23
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
25
24
|
import {insecureRandomId} from '../utils/ids'
|
|
25
|
+
import {omitProperty} from '../utils/object'
|
|
26
|
+
import {setCleanupTimeout} from './setCleanupTimeout'
|
|
26
27
|
|
|
27
28
|
interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
|
|
28
29
|
/**
|
|
@@ -182,8 +183,10 @@ export function createFetcherStore<TParams extends unknown[], TData>({
|
|
|
182
183
|
stateByParams: {
|
|
183
184
|
...prev.stateByParams,
|
|
184
185
|
[entry.key]: {
|
|
185
|
-
...
|
|
186
|
-
...
|
|
186
|
+
...omitProperty(entry, 'error'),
|
|
187
|
+
...(prev.stateByParams[entry.key]
|
|
188
|
+
? omitProperty(prev.stateByParams[entry.key], 'error')
|
|
189
|
+
: {}),
|
|
187
190
|
data,
|
|
188
191
|
},
|
|
189
192
|
},
|
|
@@ -247,14 +250,14 @@ export function createFetcherStore<TParams extends unknown[], TData>({
|
|
|
247
250
|
}))
|
|
248
251
|
|
|
249
252
|
return () => {
|
|
250
|
-
|
|
253
|
+
setCleanupTimeout(() => {
|
|
251
254
|
state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
|
|
252
255
|
const entry = prev.stateByParams[key]
|
|
253
256
|
if (!entry) return prev
|
|
254
257
|
|
|
255
258
|
const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
|
|
256
259
|
if (newSubs.length === 0) {
|
|
257
|
-
return {stateByParams:
|
|
260
|
+
return {stateByParams: omitProperty(prev.stateByParams, key)}
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
return {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {getStagingApiHost} from './getStagingApiHost'
|
|
4
|
+
|
|
5
|
+
describe('getStagingApiHost', () => {
|
|
6
|
+
it('returns staging host when __SANITY_STAGING__ is true', () => {
|
|
7
|
+
vi.stubGlobal('__SANITY_STAGING__', true)
|
|
8
|
+
expect(getStagingApiHost()).toBe('https://api.sanity.work')
|
|
9
|
+
vi.unstubAllGlobals()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns undefined when __SANITY_STAGING__ is false', () => {
|
|
13
|
+
vi.stubGlobal('__SANITY_STAGING__', false)
|
|
14
|
+
expect(getStagingApiHost()).toBeUndefined()
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns undefined when __SANITY_STAGING__ is not defined', () => {
|
|
19
|
+
expect(getStagingApiHost()).toBeUndefined()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const __SANITY_STAGING__: boolean | undefined
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the staging API host if the `__SANITY_STAGING__` build-time flag is
|
|
5
|
+
* set to `true` (mirroring how Sanity Studio detects staging builds).
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function getStagingApiHost(): string | undefined {
|
|
10
|
+
if (typeof __SANITY_STAGING__ !== 'undefined' && __SANITY_STAGING__ === true) {
|
|
11
|
+
return 'https://api.sanity.work'
|
|
12
|
+
}
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
package/src/utils/ids.test.ts
CHANGED
|
@@ -1,34 +1,6 @@
|
|
|
1
1
|
import {describe, expect, it} from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
describe('getDraftId', () => {
|
|
6
|
-
it('should add drafts prefix to non-draft ids', () => {
|
|
7
|
-
expect(getDraftId('abc123')).toBe('drafts.abc123')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('should not modify ids that already have drafts prefix', () => {
|
|
11
|
-
expect(getDraftId('drafts.abc123')).toBe('drafts.abc123')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('should handle empty string', () => {
|
|
15
|
-
expect(getDraftId('')).toBe('drafts.')
|
|
16
|
-
})
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
describe('getPublishedId', () => {
|
|
20
|
-
it('should remove drafts prefix from draft ids', () => {
|
|
21
|
-
expect(getPublishedId('drafts.abc123')).toBe('abc123')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should not modify ids that dont have drafts prefix', () => {
|
|
25
|
-
expect(getPublishedId('abc123')).toBe('abc123')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should handle empty string', () => {
|
|
29
|
-
expect(getPublishedId('')).toBe('')
|
|
30
|
-
})
|
|
31
|
-
})
|
|
3
|
+
import {insecureRandomId} from './ids'
|
|
32
4
|
|
|
33
5
|
describe('insecureRandomId', () => {
|
|
34
6
|
it('should generate 16-character string', () => {
|
package/src/utils/ids.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
export function getPublishedId(id: string): string {
|
|
2
|
-
const draftsPrefix = 'drafts.'
|
|
3
|
-
return id.startsWith(draftsPrefix) ? id.slice(draftsPrefix.length) : id
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function getDraftId(id: string): string {
|
|
7
|
-
const draftsPrefix = 'drafts.'
|
|
8
|
-
return id.startsWith(draftsPrefix) ? id : `${draftsPrefix}${id}`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
1
|
export function insecureRandomId(): string {
|
|
12
2
|
return Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('')
|
|
13
3
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {describe, expect, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {isImportError} from './isImportError'
|
|
4
|
+
|
|
5
|
+
describe('isImportError', () => {
|
|
6
|
+
test('returns false for non-Error values', () => {
|
|
7
|
+
expect(isImportError(null)).toBe(false)
|
|
8
|
+
expect(isImportError(undefined)).toBe(false)
|
|
9
|
+
expect(isImportError('Loading chunk 5 failed.')).toBe(false)
|
|
10
|
+
expect(isImportError({message: 'Loading chunk 5 failed.'})).toBe(false)
|
|
11
|
+
expect(isImportError(42)).toBe(false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('returns false for unrelated Error instances', () => {
|
|
15
|
+
expect(isImportError(new Error('Something else went wrong'))).toBe(false)
|
|
16
|
+
expect(isImportError(new TypeError('Cannot read properties of undefined'))).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('detects webpack ChunkLoadError by name', () => {
|
|
20
|
+
const err = new Error('arbitrary message')
|
|
21
|
+
err.name = 'ChunkLoadError'
|
|
22
|
+
expect(isImportError(err)).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('detects webpack numeric chunk failure messages', () => {
|
|
26
|
+
expect(isImportError(new Error('Loading chunk 5 failed.'))).toBe(true)
|
|
27
|
+
expect(
|
|
28
|
+
isImportError(new Error('Loading chunk 42 failed. (missing: https://x.com/42.abc.js)')),
|
|
29
|
+
).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('detects webpack named chunk failure messages', () => {
|
|
33
|
+
expect(isImportError(new Error('Loading chunk vendors-foo failed.'))).toBe(true)
|
|
34
|
+
expect(isImportError(new Error('Loading chunk react_vendors failed.'))).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('detects Vite "Failed to fetch dynamically imported module"', () => {
|
|
38
|
+
expect(
|
|
39
|
+
isImportError(
|
|
40
|
+
new TypeError(
|
|
41
|
+
'Failed to fetch dynamically imported module: https://example.com/assets/Home-abc123.js',
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('detects Firefox "error loading dynamically imported module"', () => {
|
|
48
|
+
expect(
|
|
49
|
+
isImportError(
|
|
50
|
+
new TypeError(
|
|
51
|
+
'error loading dynamically imported module: http://localhost:8080/src/views/Dashboard/index.vue',
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('detects Safari module-script failures with and without the "ing" suffix', () => {
|
|
58
|
+
expect(isImportError(new TypeError('Importing a module script failed.'))).toBe(true)
|
|
59
|
+
expect(isImportError(new TypeError('Import a module script failed.'))).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('detects Vite CSS preload failures', () => {
|
|
63
|
+
expect(isImportError(new Error('Unable to preload CSS for /assets/App-BBLnt7oG.css'))).toBe(
|
|
64
|
+
true,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('matches case-insensitively', () => {
|
|
69
|
+
expect(isImportError(new Error('loading chunk 1 FAILED'))).toBe(true)
|
|
70
|
+
expect(isImportError(new Error('FAILED TO FETCH DYNAMICALLY IMPORTED MODULE'))).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when the given error looks like a dynamic-import or
|
|
3
|
+
* code-split chunk-loading failure.
|
|
4
|
+
*
|
|
5
|
+
* These errors typically surface when a user has a tab open against a
|
|
6
|
+
* previously-deployed version of an app and the JavaScript or CSS chunk
|
|
7
|
+
* filenames have since changed: a fresh deployment removes the hashed assets
|
|
8
|
+
* the open tab still references. Detecting them lets the SDK trigger an
|
|
9
|
+
* automatic reload so the user gets the new build without manual intervention.
|
|
10
|
+
*
|
|
11
|
+
* Recognized shapes (webpack ChunkLoadError, Vite "Failed to fetch
|
|
12
|
+
* dynamically imported module", Firefox "error loading dynamically imported
|
|
13
|
+
* module", Safari "Importing a module script failed", and Vite "Unable to
|
|
14
|
+
* preload CSS").
|
|
15
|
+
*
|
|
16
|
+
* @param error - The value to inspect. Anything that is not an Error
|
|
17
|
+
* instance returns false.
|
|
18
|
+
* @returns True if the error matches a known import/chunk-load failure.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export function isImportError(error: unknown): boolean {
|
|
23
|
+
if (!(error instanceof Error)) return false
|
|
24
|
+
if (error.name === 'ChunkLoadError') return true
|
|
25
|
+
|
|
26
|
+
const message = error.message || ''
|
|
27
|
+
return (
|
|
28
|
+
/Loading chunk [\w-]+ failed/i.test(message) ||
|
|
29
|
+
/Failed to fetch dynamically imported module/i.test(message) ||
|
|
30
|
+
/error loading dynamically imported module/i.test(message) ||
|
|
31
|
+
/Import(?:ing)? a module script failed/i.test(message) ||
|
|
32
|
+
/Unable to preload CSS/i.test(message)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {isDeepEqual, isObject, omitProperty, pickProperties} from './object'
|
|
4
|
+
|
|
5
|
+
describe('object utils', () => {
|
|
6
|
+
describe('isObject', () => {
|
|
7
|
+
it('returns true for objects and false for primitives', () => {
|
|
8
|
+
expect(isObject({foo: 'bar'})).toBe(true)
|
|
9
|
+
expect(isObject(null)).toBe(false)
|
|
10
|
+
expect(isObject('hello')).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('omitProperty', () => {
|
|
15
|
+
it('removes a property from an object copy', () => {
|
|
16
|
+
expect(omitProperty({foo: 'bar', baz: 1}, 'foo')).toEqual({baz: 1})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns an empty object for undefined input', () => {
|
|
20
|
+
expect(omitProperty<{foo: string}, 'foo'>(undefined, 'foo')).toEqual({})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('pickProperties', () => {
|
|
25
|
+
it('copies only the requested own properties', () => {
|
|
26
|
+
expect(pickProperties({foo: 'bar', baz: 1}, ['foo'])).toEqual({foo: 'bar'})
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('isDeepEqual', () => {
|
|
31
|
+
it('matches nested plain objects and arrays', () => {
|
|
32
|
+
expect(
|
|
33
|
+
isDeepEqual({foo: [{bar: 'baz'}], qux: {count: 2}}, {foo: [{bar: 'baz'}], qux: {count: 2}}),
|
|
34
|
+
).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns false for unequal nested plain objects and arrays', () => {
|
|
38
|
+
expect(
|
|
39
|
+
isDeepEqual(
|
|
40
|
+
{foo: [{bar: 'baz'}], qux: {count: 2}},
|
|
41
|
+
{foo: [{bar: 'nope'}], qux: {count: 2}},
|
|
42
|
+
),
|
|
43
|
+
).toBe(false)
|
|
44
|
+
expect(isDeepEqual([1, {foo: 'bar'}], [1, {foo: 'baz'}])).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('matches sets and maps with equal contents', () => {
|
|
48
|
+
expect(isDeepEqual(new Set([{foo: 'bar'}, 'baz']), new Set(['baz', {foo: 'bar'}]))).toBe(true)
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
isDeepEqual(
|
|
52
|
+
new Map<string, unknown>([
|
|
53
|
+
['foo', {bar: 'baz'}],
|
|
54
|
+
['count', 2],
|
|
55
|
+
]),
|
|
56
|
+
new Map<string, unknown>([
|
|
57
|
+
['foo', {bar: 'baz'}],
|
|
58
|
+
['count', 2],
|
|
59
|
+
]),
|
|
60
|
+
),
|
|
61
|
+
).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('compares dates by timestamp', () => {
|
|
65
|
+
expect(isDeepEqual(new Date('2024-01-01'), new Date('2024-01-01'))).toBe(true)
|
|
66
|
+
expect(isDeepEqual(new Date('2024-01-01'), new Date('2024-01-02'))).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('compares regular expressions by source and flags', () => {
|
|
70
|
+
expect(isDeepEqual(/foo/gi, /foo/gi)).toBe(true)
|
|
71
|
+
expect(isDeepEqual(/foo/g, /foo/i)).toBe(false)
|
|
72
|
+
expect(isDeepEqual(/foo/g, /bar/g)).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns false for cross-shape mismatches', () => {
|
|
76
|
+
expect(isDeepEqual({foo: 'bar'}, ['foo', 'bar'] as unknown as {foo: string})).toBe(false)
|
|
77
|
+
expect(
|
|
78
|
+
isDeepEqual(
|
|
79
|
+
new Map<string, unknown>([['foo', 'bar']]) as unknown as object,
|
|
80
|
+
new Set(['foo', 'bar']) as unknown as object,
|
|
81
|
+
),
|
|
82
|
+
).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('treats non-plain objects as unequal unless they are the same reference', () => {
|
|
86
|
+
class Example {
|
|
87
|
+
constructor(public value: string) {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(isDeepEqual(new Example('a'), new Example('a'))).toBe(false)
|
|
91
|
+
const instance = new Example('a')
|
|
92
|
+
expect(isDeepEqual(instance, instance)).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|