@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
|
@@ -2,10 +2,10 @@ import {type ClientPerspective} from '@sanity/client'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type DatasetHandle,
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type DocumentResource,
|
|
6
|
+
isCanvasResource,
|
|
7
|
+
isDatasetResource,
|
|
8
|
+
isMediaLibraryResource,
|
|
9
9
|
type ReleasePerspective,
|
|
10
10
|
} from '../config/sanityConfig'
|
|
11
11
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
@@ -14,14 +14,14 @@ import {createStoreInstance, type StoreInstance} from './createStoreInstance'
|
|
|
14
14
|
import {type StoreState} from './createStoreState'
|
|
15
15
|
import {type StoreContext, type StoreDefinition} from './defineStore'
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface BoundResourceKey {
|
|
18
18
|
name: string
|
|
19
|
-
|
|
19
|
+
resource: DocumentResource
|
|
20
20
|
}
|
|
21
|
-
export interface BoundPerspectiveKey extends
|
|
21
|
+
export interface BoundPerspectiveKey extends BoundResourceKey {
|
|
22
22
|
perspective: ClientPerspective | ReleasePerspective
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
interface BoundDatasetKey {
|
|
25
25
|
name: string
|
|
26
26
|
projectId: string
|
|
27
27
|
dataset: string
|
|
@@ -166,21 +166,24 @@ export const bindActionByDataset = createActionBinder<
|
|
|
166
166
|
return {name: `${projectId}.${dataset}`, projectId, dataset}
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
-
const
|
|
169
|
+
const createResourceKey = (
|
|
170
|
+
instance: SanityInstance,
|
|
171
|
+
resource?: DocumentResource,
|
|
172
|
+
): BoundResourceKey => {
|
|
170
173
|
let name: string | undefined
|
|
171
|
-
let
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
name = `${
|
|
176
|
-
} else if (
|
|
177
|
-
name = `media-library:${
|
|
178
|
-
} else if (
|
|
179
|
-
name = `canvas:${
|
|
174
|
+
let resourceForKey: DocumentResource | undefined
|
|
175
|
+
if (resource) {
|
|
176
|
+
resourceForKey = resource
|
|
177
|
+
if (isDatasetResource(resource)) {
|
|
178
|
+
name = `${resource.projectId}.${resource.dataset}`
|
|
179
|
+
} else if (isMediaLibraryResource(resource)) {
|
|
180
|
+
name = `media-library:${resource.mediaLibraryId}`
|
|
181
|
+
} else if (isCanvasResource(resource)) {
|
|
182
|
+
name = `canvas:${resource.canvasId}`
|
|
180
183
|
} else {
|
|
181
|
-
throw new Error(`Received invalid
|
|
184
|
+
throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
|
|
182
185
|
}
|
|
183
|
-
return {name,
|
|
186
|
+
return {name, resource: resourceForKey}
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// TODO: remove reference to instance.config when we get to v3
|
|
@@ -188,30 +191,32 @@ const createSourceKey = (instance: SanityInstance, source?: DocumentSource): Bou
|
|
|
188
191
|
if (!projectId || !dataset) {
|
|
189
192
|
throw new Error('This API requires a project ID and dataset configured.')
|
|
190
193
|
}
|
|
191
|
-
return {name: `${projectId}.${dataset}`,
|
|
194
|
+
return {name: `${projectId}.${dataset}`, resource: {projectId, dataset}}
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
/**
|
|
195
|
-
* Binds an action to a store that's scoped to a specific document
|
|
198
|
+
* Binds an action to a store that's scoped to a specific document resource.
|
|
196
199
|
**/
|
|
197
|
-
export const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
export const bindActionByResource = createActionBinder<
|
|
201
|
+
BoundResourceKey,
|
|
202
|
+
// this implies resources is optional to keep backwards compatibility
|
|
203
|
+
// but in reality, we'll always pass a resource (since we'll defer to the instance until v3)
|
|
204
|
+
[{resource?: DocumentResource}, ...unknown[]]
|
|
205
|
+
>((instance, {resource}) => {
|
|
206
|
+
return createResourceKey(instance, resource)
|
|
202
207
|
})
|
|
203
208
|
|
|
204
209
|
/**
|
|
205
|
-
* Binds an action to a store that's scoped to a specific document
|
|
210
|
+
* Binds an action to a store that's scoped to a specific document resource and perspective.
|
|
206
211
|
*
|
|
207
212
|
* @remarks
|
|
208
|
-
* This creates actions that operate on state isolated to a specific document
|
|
209
|
-
* Different document
|
|
213
|
+
* This creates actions that operate on state isolated to a specific document resource and perspective.
|
|
214
|
+
* Different document resources and perspectives will have separate states.
|
|
210
215
|
*
|
|
211
216
|
* This is mostly useful for stores that do batch fetching operations, since the query store
|
|
212
217
|
* can isolate single queries by perspective.
|
|
213
218
|
*
|
|
214
|
-
* @throws Error if
|
|
219
|
+
* @throws Error if resource or perspective is missing from the Sanity instance config
|
|
215
220
|
*
|
|
216
221
|
* @example
|
|
217
222
|
* ```ts
|
|
@@ -222,11 +227,11 @@ export const bindActionBySource = createActionBinder<
|
|
|
222
227
|
* // ...
|
|
223
228
|
* })
|
|
224
229
|
*
|
|
225
|
-
* // Create
|
|
226
|
-
* export const fetchDocuments =
|
|
230
|
+
* // Create resource-and-perspective-specific actions
|
|
231
|
+
* export const fetchDocuments = bindActionByResourceAndPerspective(
|
|
227
232
|
* documentStore,
|
|
228
233
|
* ({instance, state}, documentId) => {
|
|
229
|
-
* // This state is isolated to the specific document
|
|
234
|
+
* // This state is isolated to the specific document resource and perspective
|
|
230
235
|
* // ...fetch logic...
|
|
231
236
|
* }
|
|
232
237
|
* )
|
|
@@ -235,11 +240,11 @@ export const bindActionBySource = createActionBinder<
|
|
|
235
240
|
* fetchDocument(sanityInstance, 'doc123')
|
|
236
241
|
* ```
|
|
237
242
|
*/
|
|
238
|
-
export const
|
|
243
|
+
export const bindActionByResourceAndPerspective = createActionBinder<
|
|
239
244
|
BoundPerspectiveKey,
|
|
240
245
|
[DatasetHandle, ...unknown[]]
|
|
241
246
|
>((instance, options): BoundPerspectiveKey => {
|
|
242
|
-
const {
|
|
247
|
+
const {resource, perspective} = options
|
|
243
248
|
// TODO: remove reference to instance.config.perspective when we get to v3
|
|
244
249
|
const utilizedPerspective = perspective ?? instance.config.perspective ?? 'drafts'
|
|
245
250
|
let perspectiveKey: string
|
|
@@ -251,11 +256,11 @@ export const bindActionBySourceAndPerspective = createActionBinder<
|
|
|
251
256
|
// "StackablePerspective", shouldn't be a common case, but just in case
|
|
252
257
|
perspectiveKey = JSON.stringify(utilizedPerspective)
|
|
253
258
|
}
|
|
254
|
-
const sourceKey =
|
|
259
|
+
const sourceKey = createResourceKey(instance, resource)
|
|
255
260
|
|
|
256
261
|
return {
|
|
257
262
|
name: `${sourceKey.name}:${perspectiveKey}`,
|
|
258
|
-
|
|
263
|
+
resource: sourceKey.resource,
|
|
259
264
|
perspective: utilizedPerspective,
|
|
260
265
|
}
|
|
261
266
|
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {pick} from 'lodash-es'
|
|
2
|
-
|
|
3
1
|
import {type SanityConfig} from '../config/sanityConfig'
|
|
4
2
|
import {insecureRandomId} from '../utils/ids'
|
|
5
3
|
import {createLogger, type InstanceContext} from '../utils/logger'
|
|
4
|
+
import {pickProperties} from '../utils/object'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Represents a Sanity.io resource instance with its own configuration and lifecycle
|
|
@@ -158,7 +157,7 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
|
|
|
158
157
|
},
|
|
159
158
|
match: (targetConfig) => {
|
|
160
159
|
if (
|
|
161
|
-
Object.entries(
|
|
160
|
+
Object.entries(pickProperties(targetConfig, ['auth', 'projectId', 'dataset'])).every(
|
|
162
161
|
([key, value]) => config[key as keyof SanityConfig] === value,
|
|
163
162
|
)
|
|
164
163
|
) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {filter, firstValueFrom, timeout} from 'rxjs'
|
|
1
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
3
|
|
|
3
4
|
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
@@ -194,4 +195,65 @@ describe('createStateSourceAction', () => {
|
|
|
194
195
|
expect(context1.instance).toBe(instance)
|
|
195
196
|
expect(context2.instance).toBe(secondInstance)
|
|
196
197
|
})
|
|
198
|
+
|
|
199
|
+
it('correctly observes values defined in the onSubscribe', async () => {
|
|
200
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
201
|
+
const source = createStateSourceAction({
|
|
202
|
+
selector: selector,
|
|
203
|
+
onSubscribe() {
|
|
204
|
+
state.set('update', {count: 1})
|
|
205
|
+
},
|
|
206
|
+
})({state, instance, key: null})
|
|
207
|
+
|
|
208
|
+
const value = await firstValueFrom(
|
|
209
|
+
source.observable.pipe(
|
|
210
|
+
filter((i) => i === 1),
|
|
211
|
+
timeout(10),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
expect(value).toBe(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('only invokes selector once on changes', () => {
|
|
218
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
219
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
220
|
+
|
|
221
|
+
expect(selector).toBeCalledTimes(0)
|
|
222
|
+
|
|
223
|
+
// Now it should be called once:
|
|
224
|
+
const sub = source.observable.subscribe()
|
|
225
|
+
expect(selector).toBeCalledTimes(1)
|
|
226
|
+
|
|
227
|
+
// The observable should be shared so this shouldn't invoke it first.
|
|
228
|
+
const sub2 = source.observable.subscribe()
|
|
229
|
+
expect(selector).toBeCalledTimes(1)
|
|
230
|
+
|
|
231
|
+
// Updating the value should only invoke it once:
|
|
232
|
+
state.set('update', {count: 1})
|
|
233
|
+
expect(selector).toBeCalledTimes(2)
|
|
234
|
+
|
|
235
|
+
sub2.unsubscribe()
|
|
236
|
+
sub.unsubscribe()
|
|
237
|
+
|
|
238
|
+
// Once everyone has unsubscribed it should be invoked again.
|
|
239
|
+
const sub3 = source.observable.subscribe()
|
|
240
|
+
expect(selector).toBeCalledTimes(3)
|
|
241
|
+
sub3.unsubscribe()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('only subscribes once when mixing subscribe/observable', () => {
|
|
245
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
246
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
247
|
+
|
|
248
|
+
expect(selector).toBeCalledTimes(0)
|
|
249
|
+
|
|
250
|
+
const sub = source.observable.subscribe()
|
|
251
|
+
expect(selector).toBeCalledTimes(1)
|
|
252
|
+
|
|
253
|
+
const sub2 = source.subscribe()
|
|
254
|
+
expect(selector).toBeCalledTimes(1)
|
|
255
|
+
|
|
256
|
+
sub.unsubscribe()
|
|
257
|
+
sub2()
|
|
258
|
+
})
|
|
197
259
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {distinctUntilChanged, map, Observable,
|
|
1
|
+
import {defer, distinctUntilChanged, finalize, map, Observable, shareReplay, skip} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {type StoreAction} from './createActionBinder'
|
|
4
4
|
import {type SanityInstance} from './createSanityInstance'
|
|
@@ -187,8 +187,7 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
187
187
|
function stateSourceAction(context: StoreContext<TState, TKey>, ...params: TParams) {
|
|
188
188
|
const {state, instance} = context
|
|
189
189
|
|
|
190
|
-
const getCurrent = () => {
|
|
191
|
-
const currentState = state.get()
|
|
190
|
+
const getCurrent = (currentState: TState) => {
|
|
192
191
|
if (typeof currentState !== 'object' || currentState === null) {
|
|
193
192
|
throw new Error(
|
|
194
193
|
`Expected store state to be an object but got "${typeof currentState}" instead`,
|
|
@@ -208,53 +207,49 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
208
207
|
return selector(selectorContext, ...params)
|
|
209
208
|
}
|
|
210
209
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const cleanup = subscribeHandler?.(context, ...params)
|
|
210
|
+
// `state.observable` will emit the current value immediately and
|
|
211
|
+
// hence we inherit the same behavior here.
|
|
212
|
+
let values = state.observable.pipe(map(getCurrent), distinctUntilChanged(isEqual))
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
if (subscribeHandler) {
|
|
215
|
+
values = withSubscribeHook(values, () => subscribeHandler(context, ...params))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Share but replay the latest value so every subscriber gets an
|
|
219
|
+
// initial synchronous emission, matching `state.observable`. That keeps
|
|
220
|
+
// `skip(1)` in `subscribe()` aligned with "skip current snapshot" rather than
|
|
221
|
+
// silently eating the first real update after multicasting.
|
|
222
|
+
const sharedValues = values.pipe(shareReplay({bufferSize: 1, refCount: true}))
|
|
223
|
+
|
|
224
|
+
const subscribe = (onStoreChanged?: () => void) => {
|
|
225
|
+
const subscription = sharedValues.pipe(skip(1)).subscribe({
|
|
226
|
+
next: () => onStoreChanged?.(),
|
|
227
|
+
// Propagate selector errors to both subscription types
|
|
228
|
+
error: () => onStoreChanged?.(),
|
|
229
|
+
})
|
|
231
230
|
|
|
232
231
|
return () => {
|
|
233
232
|
subscription.unsubscribe()
|
|
234
|
-
cleanup?.()
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
// Create shared observable that handles multiple subscribers efficiently
|
|
239
|
-
const observable = new Observable<TReturn>((observer) => {
|
|
240
|
-
const emitCurrent = () => {
|
|
241
|
-
try {
|
|
242
|
-
observer.next(getCurrent())
|
|
243
|
-
} catch (error) {
|
|
244
|
-
observer.error(error)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Emit immediately on subscription
|
|
248
|
-
emitCurrent()
|
|
249
|
-
return subscribe(emitCurrent)
|
|
250
|
-
}).pipe(share())
|
|
251
|
-
|
|
252
236
|
return {
|
|
253
|
-
getCurrent,
|
|
237
|
+
getCurrent: () => getCurrent(state.get()),
|
|
254
238
|
subscribe,
|
|
255
|
-
observable,
|
|
239
|
+
observable: sharedValues,
|
|
256
240
|
}
|
|
257
241
|
}
|
|
258
242
|
|
|
259
243
|
return stateSourceAction
|
|
260
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a new Observable which wraps an existing Observable which will invoke
|
|
248
|
+
* the function when a new subscriber appears.
|
|
249
|
+
*/
|
|
250
|
+
function withSubscribeHook<T>(obs: Observable<T>, fn: () => void | (() => void)): Observable<T> {
|
|
251
|
+
return defer(() => {
|
|
252
|
+
const cleanup = fn()
|
|
253
|
+
return cleanup ? obs.pipe(finalize(() => cleanup())) : obs
|
|
254
|
+
})
|
|
255
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {defineEvent} from '@sanity/telemetry'
|
|
2
|
+
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const SDKDevSessionStarted = defineEvent<{
|
|
5
|
+
version: string
|
|
6
|
+
projectId: string
|
|
7
|
+
perspective: string
|
|
8
|
+
authMethod: string
|
|
9
|
+
}>({
|
|
10
|
+
name: 'SDK Dev Session Started',
|
|
11
|
+
version: 1,
|
|
12
|
+
description: 'SDK instance created in development mode',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export const SDKHookMounted = defineEvent<{
|
|
17
|
+
hookName: string
|
|
18
|
+
}>({
|
|
19
|
+
name: 'SDK Hook Mounted',
|
|
20
|
+
version: 1,
|
|
21
|
+
description: 'An SDK hook was mounted for the first time in this session',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** @internal */
|
|
25
|
+
export const SDKDevSessionEnded = defineEvent<{
|
|
26
|
+
durationSeconds: number
|
|
27
|
+
hooksUsed: string[]
|
|
28
|
+
}>({
|
|
29
|
+
name: 'SDK Dev Session Ended',
|
|
30
|
+
version: 1,
|
|
31
|
+
description: 'SDK instance disposed in development mode',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export const SDKDevError = defineEvent<{
|
|
36
|
+
errorType: string
|
|
37
|
+
hookName: string
|
|
38
|
+
}>({
|
|
39
|
+
name: 'SDK Dev Error',
|
|
40
|
+
version: 1,
|
|
41
|
+
description: 'Runtime error caught during SDK development',
|
|
42
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {isDevMode} from './devMode'
|
|
4
|
+
|
|
5
|
+
describe('isDevMode', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.unstubAllEnvs()
|
|
8
|
+
vi.unstubAllGlobals()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('returns false when NODE_ENV is production', () => {
|
|
12
|
+
vi.stubEnv('NODE_ENV', 'production')
|
|
13
|
+
vi.stubGlobal('window', undefined)
|
|
14
|
+
expect(isDevMode()).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns true when running on localhost', () => {
|
|
18
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
19
|
+
vi.stubGlobal('window', {
|
|
20
|
+
location: {href: 'http://localhost:3000/'},
|
|
21
|
+
})
|
|
22
|
+
expect(isDevMode()).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns true when running on 127.0.0.1', () => {
|
|
26
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
27
|
+
vi.stubGlobal('window', {
|
|
28
|
+
location: {href: 'http://127.0.0.1:3000/'},
|
|
29
|
+
})
|
|
30
|
+
expect(isDevMode()).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns false for a non-local URL', () => {
|
|
34
|
+
vi.stubEnv('NODE_ENV', 'test')
|
|
35
|
+
vi.stubGlobal('window', {
|
|
36
|
+
location: {href: 'https://myapp.sanity.studio/'},
|
|
37
|
+
})
|
|
38
|
+
expect(isDevMode()).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns true when NODE_ENV is development and no window', () => {
|
|
42
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
43
|
+
vi.stubGlobal('window', undefined)
|
|
44
|
+
expect(isDevMode()).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns false when NODE_ENV is test and no window', () => {
|
|
48
|
+
vi.stubEnv('NODE_ENV', 'test')
|
|
49
|
+
vi.stubGlobal('window', undefined)
|
|
50
|
+
expect(isDevMode()).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether the current URL points to a local development server.
|
|
3
|
+
*
|
|
4
|
+
* @param win - The window object to check
|
|
5
|
+
* @returns True if running on localhost or 127.0.0.1
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
function isLocalUrl(win: Window): boolean {
|
|
9
|
+
const url = win.location?.href
|
|
10
|
+
if (!url) return false
|
|
11
|
+
return (
|
|
12
|
+
url.startsWith('http://localhost') ||
|
|
13
|
+
url.startsWith('https://localhost') ||
|
|
14
|
+
url.startsWith('http://127.0.0.1') ||
|
|
15
|
+
url.startsWith('https://127.0.0.1')
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether the SDK should enable dev-mode telemetry.
|
|
21
|
+
*
|
|
22
|
+
* Combines a browser URL check (localhost/127.0.0.1) with a Node.js
|
|
23
|
+
* environment variable check (`NODE_ENV === 'development'`). Returns
|
|
24
|
+
* false in production environments so bundlers can tree-shake the
|
|
25
|
+
* telemetry code path entirely.
|
|
26
|
+
*
|
|
27
|
+
* @returns True if the SDK is running in a development environment
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export function isDevMode(): boolean {
|
|
31
|
+
if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
return isLocalUrl(window)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'development'
|
|
40
|
+
}
|