@sanity/sdk 2.9.0 → 2.11.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.
Files changed (73) hide show
  1. package/dist/_chunks-dts/utils.d.ts +295 -69
  2. package/dist/_chunks-es/_internal.js +3 -14
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +129 -59
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/version.js +1 -1
  7. package/dist/_exports/_internal.d.ts +16 -2
  8. package/dist/_exports/_internal.js +3 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +275 -149
  11. package/dist/index.js.map +1 -1
  12. package/package.json +11 -15
  13. package/src/_exports/_internal.ts +1 -0
  14. package/src/_exports/index.ts +33 -2
  15. package/src/agent/agentActions.ts +21 -25
  16. package/src/client/clientStore.test.ts +24 -60
  17. package/src/client/clientStore.ts +49 -56
  18. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  19. package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
  20. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  21. package/src/comlink/node/actions/releaseNode.test.ts +3 -3
  22. package/src/config/sanityConfig.ts +72 -13
  23. package/src/document/applyDocumentActions.test.ts +7 -7
  24. package/src/document/applyDocumentActions.ts +5 -5
  25. package/src/document/documentStore.test.ts +68 -62
  26. package/src/document/documentStore.ts +33 -38
  27. package/src/document/processActions.ts +2 -2
  28. package/src/document/reducers.ts +4 -4
  29. package/src/document/sharedListener.ts +5 -7
  30. package/src/organization/organization.test-d.ts +102 -0
  31. package/src/organization/organization.test.ts +138 -0
  32. package/src/organization/organization.ts +166 -0
  33. package/src/organizations/organizations.test-d.ts +77 -0
  34. package/src/organizations/organizations.test.ts +150 -0
  35. package/src/organizations/organizations.ts +132 -0
  36. package/src/presence/bifurTransport.test.ts +46 -6
  37. package/src/presence/bifurTransport.ts +13 -1
  38. package/src/presence/presenceStore.test.ts +101 -5
  39. package/src/presence/presenceStore.ts +96 -24
  40. package/src/preview/getPreviewState.ts +1 -1
  41. package/src/preview/previewProjectionUtils.test.ts +4 -4
  42. package/src/preview/previewProjectionUtils.ts +6 -7
  43. package/src/preview/resolvePreview.ts +5 -1
  44. package/src/project/project.test-d.ts +93 -0
  45. package/src/project/project.test.ts +108 -10
  46. package/src/project/project.ts +152 -26
  47. package/src/projection/getProjectionState.ts +4 -4
  48. package/src/projection/projectionStore.test.ts +2 -2
  49. package/src/projection/resolveProjection.ts +2 -2
  50. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  51. package/src/projection/subscribeToStateAndFetchBatches.ts +11 -15
  52. package/src/projects/projects.test-d.ts +38 -0
  53. package/src/projects/projects.test.ts +104 -38
  54. package/src/projects/projects.ts +74 -14
  55. package/src/query/queryStore.test.ts +12 -12
  56. package/src/query/queryStore.ts +10 -11
  57. package/src/query/reducers.ts +3 -3
  58. package/src/releases/getPerspectiveState.ts +5 -5
  59. package/src/releases/releasesStore.test.ts +6 -6
  60. package/src/releases/releasesStore.ts +9 -9
  61. package/src/store/createActionBinder.test.ts +31 -31
  62. package/src/store/createActionBinder.ts +43 -38
  63. package/src/store/createSanityInstance.ts +5 -6
  64. package/src/telemetry/devMode.test.ts +8 -0
  65. package/src/telemetry/devMode.ts +10 -9
  66. package/src/telemetry/initTelemetry.test.ts +0 -17
  67. package/src/telemetry/initTelemetry.ts +2 -12
  68. package/src/users/reducers.ts +3 -4
  69. package/src/utils/createFetcherStore.ts +6 -4
  70. package/src/utils/isImportError.test.ts +72 -0
  71. package/src/utils/isImportError.ts +34 -0
  72. package/src/utils/object.test.ts +95 -0
  73. package/src/utils/object.ts +142 -0
@@ -2,10 +2,10 @@ import {type ClientPerspective} from '@sanity/client'
2
2
 
3
3
  import {
4
4
  type DatasetHandle,
5
- type DocumentSource,
6
- isCanvasSource,
7
- isDatasetSource,
8
- isMediaLibrarySource,
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 BoundSourceKey {
17
+ export interface BoundResourceKey {
18
18
  name: string
19
- source: DocumentSource
19
+ resource: DocumentResource
20
20
  }
21
- export interface BoundPerspectiveKey extends BoundSourceKey {
21
+ export interface BoundPerspectiveKey extends BoundResourceKey {
22
22
  perspective: ClientPerspective | ReleasePerspective
23
23
  }
24
- export interface BoundDatasetKey {
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 createSourceKey = (instance: SanityInstance, source?: DocumentSource): BoundSourceKey => {
169
+ const createResourceKey = (
170
+ instance: SanityInstance,
171
+ resource?: DocumentResource,
172
+ ): BoundResourceKey => {
170
173
  let name: string | undefined
171
- let sourceForKey: DocumentSource | undefined
172
- if (source) {
173
- sourceForKey = source
174
- if (isDatasetSource(source)) {
175
- name = `${source.projectId}.${source.dataset}`
176
- } else if (isMediaLibrarySource(source)) {
177
- name = `media-library:${source.mediaLibraryId}`
178
- } else if (isCanvasSource(source)) {
179
- name = `canvas:${source.canvasId}`
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 source: ${JSON.stringify(source)}`)
184
+ throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
182
185
  }
183
- return {name, source: sourceForKey}
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}`, source: {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 source.
198
+ * Binds an action to a store that's scoped to a specific document resource.
196
199
  **/
197
- export const bindActionBySource = createActionBinder<
198
- BoundSourceKey,
199
- [{source?: DocumentSource}, ...unknown[]]
200
- >((instance, {source}) => {
201
- return createSourceKey(instance, source)
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 source and perspective.
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 source and perspective.
209
- * Different document sources and perspectives will have separate states.
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 source or perspective is missing from the Sanity instance config
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 source-and-perspective-specific actions
226
- * export const fetchDocuments = bindActionBySourceAndPerspective(
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 source and perspective
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 bindActionBySourceAndPerspective = createActionBinder<
243
+ export const bindActionByResourceAndPerspective = createActionBinder<
239
244
  BoundPerspectiveKey,
240
245
  [DatasetHandle, ...unknown[]]
241
246
  >((instance, options): BoundPerspectiveKey => {
242
- const {source, perspective} = options
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 = createSourceKey(instance, source)
259
+ const sourceKey = createResourceKey(instance, resource)
255
260
 
256
261
  return {
257
262
  name: `${sourceKey.name}:${perspectiveKey}`,
258
- source: sourceKey.source,
263
+ resource: sourceKey.resource,
259
264
  perspective: utilizedPerspective,
260
265
  }
261
266
  })
@@ -1,12 +1,10 @@
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
9
- * @remarks Instances form a hierarchy through parent/child relationships
10
8
  *
11
9
  * @public
12
10
  */
@@ -19,7 +17,6 @@ export interface SanityInstance {
19
17
 
20
18
  /**
21
19
  * Resolved configuration for this instance
22
- * @remarks Merges values from parent instances where appropriate
23
20
  */
24
21
  readonly config: SanityConfig
25
22
 
@@ -45,13 +42,14 @@ export interface SanityInstance {
45
42
  /**
46
43
  * Gets the parent instance in the hierarchy
47
44
  * @returns Parent instance or undefined if this is the root
45
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
48
46
  */
49
47
  getParent(): SanityInstance | undefined
50
48
 
51
49
  /**
52
50
  * Creates a child instance with merged configuration
53
51
  * @param config - Configuration to merge with parent values
54
- * @remarks Child instances inherit parent configuration but can override values
52
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
55
53
  */
56
54
  createChild(config: SanityConfig): SanityInstance
57
55
 
@@ -60,6 +58,7 @@ export interface SanityInstance {
60
58
  * matches the given target config using a shallow comparison.
61
59
  * @param targetConfig - A partial configuration object containing key-value pairs to match.
62
60
  * @returns The first matching instance or undefined if no match is found.
61
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
63
62
  */
64
63
  match(targetConfig: Partial<SanityConfig>): SanityInstance | undefined
65
64
  }
@@ -158,7 +157,7 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
158
157
  },
159
158
  match: (targetConfig) => {
160
159
  if (
161
- Object.entries(pick(targetConfig, 'auth', 'projectId', 'dataset')).every(
160
+ Object.entries(pickProperties(targetConfig, ['auth', 'projectId', 'dataset'])).every(
162
161
  ([key, value]) => config[key as keyof SanityConfig] === value,
163
162
  )
164
163
  ) {
@@ -30,6 +30,14 @@ describe('isDevMode', () => {
30
30
  expect(isDevMode()).toBe(true)
31
31
  })
32
32
 
33
+ it('returns true on localhost even when NODE_ENV is production', () => {
34
+ vi.stubEnv('NODE_ENV', 'production')
35
+ vi.stubGlobal('window', {
36
+ location: {href: 'http://localhost:3000/'},
37
+ })
38
+ expect(isDevMode()).toBe(true)
39
+ })
40
+
33
41
  it('returns false for a non-local URL', () => {
34
42
  vi.stubEnv('NODE_ENV', 'test')
35
43
  vi.stubGlobal('window', {
@@ -17,21 +17,22 @@ function isLocalUrl(win: Window): boolean {
17
17
  }
18
18
 
19
19
  /**
20
- * Determines whether the SDK should enable dev-mode telemetry.
20
+ * Determines whether the SDK should enable dev-mode telemetry for the
21
+ * SDK consumer (i.e. a developer building an app with `@sanity/sdk`).
21
22
  *
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.
23
+ * Browser: returns true only when the URL is `localhost` or `127.0.0.1`.
24
+ * The URL check is the primary signal because consumer bundlers may or
25
+ * may not forward `NODE_ENV` to the browser reliably.
26
+ *
27
+ * Node (scripts / non-browser): falls back to `NODE_ENV === 'development'`.
28
+ *
29
+ * Bracket-notation `process.env['NODE_ENV']` is used to avoid bundler
30
+ * dead-code replacement.
26
31
  *
27
32
  * @returns True if the SDK is running in a development environment
28
33
  * @internal
29
34
  */
30
35
  export function isDevMode(): boolean {
31
- if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') {
32
- return false
33
- }
34
-
35
36
  if (typeof window !== 'undefined') {
36
37
  return isLocalUrl(window)
37
38
  }
@@ -205,21 +205,4 @@ describe('initTelemetry', () => {
205
205
 
206
206
  instance.dispose()
207
207
  })
208
-
209
- it('finds manager through parent-child instance chain', async () => {
210
- vi.mocked(isDevMode).mockReturnValue(true)
211
-
212
- const root = createSanityInstance()
213
- const child = root.createChild({})
214
-
215
- initTelemetry(root, 'abc123')
216
- await flushPromises()
217
-
218
- trackHookMounted(child, 'useUsers')
219
-
220
- const manager = vi.mocked(createTelemetryManager).mock.results[0].value
221
- expect(manager.logHookFirstUsed).toHaveBeenCalledWith('useUsers')
222
-
223
- root.dispose()
224
- })
225
208
  })
@@ -187,19 +187,9 @@ export function trackHookMounted(instance: SanityInstance, hookName: string): vo
187
187
  }
188
188
 
189
189
  function findManager(instance: SanityInstance): TelemetryManager | undefined {
190
- let current: SanityInstance | undefined = instance
191
- while (current) {
192
- const manager = telemetryManagers.get(current)
193
- if (manager) return manager
194
- current = typeof current.getParent === 'function' ? current.getParent() : undefined
195
- }
196
- return undefined
190
+ return telemetryManagers.get(instance)
197
191
  }
198
192
 
199
193
  function getRootInstance(instance: SanityInstance): SanityInstance {
200
- let current = instance
201
- while (typeof current.getParent === 'function' && current.getParent()) {
202
- current = current.getParent()!
203
- }
204
- return current
194
+ return instance
205
195
  }
@@ -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: omit(prev.users, key)}
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: omit(prev.users, key)}
85
+ return {...prev, users: omitProperty(prev.users, key)}
87
86
  }
88
87
 
89
88
  export const initializeRequest =
@@ -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,7 @@ 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
26
  import {setCleanupTimeout} from './setCleanupTimeout'
27
27
 
28
28
  interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
@@ -183,8 +183,10 @@ export function createFetcherStore<TParams extends unknown[], TData>({
183
183
  stateByParams: {
184
184
  ...prev.stateByParams,
185
185
  [entry.key]: {
186
- ...omit(entry, 'error'),
187
- ...omit(prev.stateByParams[entry.key], 'error'),
186
+ ...omitProperty(entry, 'error'),
187
+ ...(prev.stateByParams[entry.key]
188
+ ? omitProperty(prev.stateByParams[entry.key], 'error')
189
+ : {}),
188
190
  data,
189
191
  },
190
192
  },
@@ -255,7 +257,7 @@ export function createFetcherStore<TParams extends unknown[], TData>({
255
257
 
256
258
  const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
257
259
  if (newSubs.length === 0) {
258
- return {stateByParams: omit(prev.stateByParams, key)}
260
+ return {stateByParams: omitProperty(prev.stateByParams, key)}
259
261
  }
260
262
 
261
263
  return {
@@ -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
+ })