@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23

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 (71) hide show
  1. package/dist/index.d.ts +502 -3460
  2. package/dist/index.js +400 -465
  3. package/dist/index.js.map +1 -1
  4. package/package.json +17 -15
  5. package/src/_exports/index.ts +4 -5
  6. package/src/components/SDKProvider.test.tsx +78 -54
  7. package/src/components/SDKProvider.tsx +31 -26
  8. package/src/components/SanityApp.test.tsx +121 -15
  9. package/src/components/SanityApp.tsx +26 -15
  10. package/src/components/auth/AuthBoundary.test.tsx +32 -14
  11. package/src/components/auth/AuthBoundary.tsx +53 -23
  12. package/src/components/auth/LoginCallback.test.tsx +19 -6
  13. package/src/components/auth/LoginCallback.tsx +2 -11
  14. package/src/components/auth/LoginError.test.tsx +12 -4
  15. package/src/components/auth/LoginError.tsx +13 -21
  16. package/src/components/auth/LoginFooter.test.tsx +7 -3
  17. package/src/context/ResourceProvider.test.tsx +157 -0
  18. package/src/context/ResourceProvider.tsx +111 -0
  19. package/src/context/SanityInstanceContext.ts +1 -1
  20. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  21. package/src/hooks/client/useClient.ts +2 -1
  22. package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
  23. package/src/hooks/comlink/useManageFavorite.ts +37 -13
  24. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
  25. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
  26. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  27. package/src/hooks/context/useSanityInstance.ts +66 -26
  28. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
  29. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
  30. package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
  31. package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
  32. package/src/hooks/datasets/useDatasets.ts +15 -4
  33. package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
  34. package/src/hooks/document/useApplyDocumentActions.ts +6 -31
  35. package/src/hooks/document/useDocument.test.ts +2 -2
  36. package/src/hooks/document/useDocument.ts +40 -19
  37. package/src/hooks/document/useDocumentEvent.test.ts +2 -3
  38. package/src/hooks/document/useDocumentEvent.ts +7 -11
  39. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  40. package/src/hooks/document/useDocumentPermissions.ts +31 -23
  41. package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
  42. package/src/hooks/document/useEditDocument.test.ts +2 -3
  43. package/src/hooks/document/useEditDocument.ts +43 -29
  44. package/src/hooks/documents/useDocuments.test.tsx +30 -3
  45. package/src/hooks/documents/useDocuments.ts +20 -7
  46. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  47. package/src/hooks/helpers/createCallbackHook.tsx +2 -3
  48. package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
  49. package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
  50. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
  51. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
  52. package/src/hooks/preview/usePreview.test.tsx +66 -7
  53. package/src/hooks/preview/usePreview.tsx +17 -12
  54. package/src/hooks/projection/useProjection.test.tsx +68 -3
  55. package/src/hooks/projection/useProjection.ts +21 -24
  56. package/src/hooks/projects/useProject.ts +7 -4
  57. package/src/hooks/query/useQuery.ts +32 -14
  58. package/src/hooks/users/useUsers.test.tsx +330 -0
  59. package/src/hooks/users/useUsers.ts +65 -52
  60. package/src/components/Login/LoginLinks.test.tsx +0 -90
  61. package/src/components/Login/LoginLinks.tsx +0 -58
  62. package/src/components/auth/Login.test.tsx +0 -27
  63. package/src/components/auth/Login.tsx +0 -39
  64. package/src/components/auth/LoginLayout.test.tsx +0 -19
  65. package/src/components/auth/LoginLayout.tsx +0 -69
  66. package/src/components/auth/authTestHelpers.tsx +0 -11
  67. package/src/context/SanityProvider.test.tsx +0 -25
  68. package/src/context/SanityProvider.tsx +0 -50
  69. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  70. package/src/hooks/auth/useLoginUrls.tsx +0 -52
  71. package/src/hooks/users/useUsers.test.ts +0 -163
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "0.0.0-alpha.21",
3
+ "version": "0.0.0-alpha.23",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -43,33 +43,35 @@
43
43
  "prettier": "@sanity/prettier-config",
44
44
  "dependencies": {
45
45
  "@sanity/logos": "^2.1.13",
46
- "@sanity/message-protocol": "^0.6.0",
46
+ "@sanity/message-protocol": "^0.7.0",
47
47
  "@sanity/types": "^3.78.1",
48
+ "@types/lodash-es": "^4.17.12",
49
+ "lodash-es": "^4.17.21",
48
50
  "react-error-boundary": "^5.0.0",
49
51
  "rxjs": "^7.8.1",
50
- "@sanity/sdk": "0.0.0-alpha.20"
52
+ "@sanity/sdk": "0.0.0-alpha.23"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@sanity/browserslist-config": "^1.0.5",
54
- "@sanity/client": "^6.28.3",
56
+ "@sanity/client": "^6.28.4",
55
57
  "@sanity/comlink": "^3.0.1",
56
- "@sanity/pkg-utils": "^6.13.4",
58
+ "@sanity/pkg-utils": "^7.2.2",
57
59
  "@sanity/prettier-config": "^1.0.3",
58
60
  "@testing-library/jest-dom": "^6.6.3",
59
- "@testing-library/react": "^16.2.0",
60
- "@types/react": "^19.0.10",
61
- "@types/react-dom": "^19.0.4",
61
+ "@testing-library/react": "^16.3.0",
62
+ "@types/react": "^19.1.0",
63
+ "@types/react-dom": "^19.1.1",
62
64
  "@vitejs/plugin-react": "^4.3.4",
63
- "@vitest/coverage-v8": "3.0.9",
64
- "babel-plugin-react-compiler": "19.0.0-beta-bafa41b-20250307",
65
+ "@vitest/coverage-v8": "3.1.1",
66
+ "babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
65
67
  "eslint": "^9.22.0",
66
68
  "jsdom": "^25.0.1",
67
69
  "prettier": "^3.5.3",
68
- "react": "^19.0.0",
69
- "react-dom": "^19.0.0",
70
+ "react": "^19.1.0",
71
+ "react-dom": "^19.1.0",
70
72
  "typescript": "^5.7.3",
71
- "vite": "^6.2.2",
72
- "vitest": "^3.0.9",
73
+ "vite": "^6.2.4",
74
+ "vitest": "^3.1.1",
73
75
  "@repo/config-eslint": "0.0.0",
74
76
  "@repo/config-test": "0.0.1",
75
77
  "@repo/package.config": "0.0.1",
@@ -89,7 +91,7 @@
89
91
  "build": "pkg build --strict --clean --check",
90
92
  "clean": "rimraf dist",
91
93
  "dev": "pkg watch",
92
- "docs": "typedoc --out docs --tsconfig ../../tsconfig.tsdoc.json",
94
+ "docs": "typedoc --out docs --tsconfig ./tsconfig.dist.json",
93
95
  "format": "prettier --write --cache --ignore-unknown .",
94
96
  "lint": "eslint .",
95
97
  "test": "vitest run",
@@ -1,14 +1,13 @@
1
1
  export {AuthBoundary} from '../components/auth/AuthBoundary'
2
2
  export {SanityApp, type SanityAppProps} from '../components/SanityApp'
3
3
  export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
4
- export type {SanityProviderProps} from '../context/SanityProvider'
5
- export {SanityProvider} from '../context/SanityProvider'
4
+ export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
6
5
  export {useAuthState} from '../hooks/auth/useAuthState'
7
6
  export {useAuthToken} from '../hooks/auth/useAuthToken'
8
7
  export {useCurrentUser} from '../hooks/auth/useCurrentUser'
9
8
  export {useDashboardOrganizationId} from '../hooks/auth/useDashboardOrganizationId'
10
9
  export {useHandleAuthCallback} from '../hooks/auth/useHandleAuthCallback'
11
- export {useLoginUrls} from '../hooks/auth/useLoginUrls'
10
+ export {useLoginUrl} from '../hooks/auth/useLoginUrl'
12
11
  export {useLogOut} from '../hooks/auth/useLogOut'
13
12
  export {useClient} from '../hooks/client/useClient'
14
13
  export {
@@ -27,7 +26,7 @@ export {
27
26
  } from '../hooks/comlink/useWindowConnection'
28
27
  export {useSanityInstance} from '../hooks/context/useSanityInstance'
29
28
  export {useNavigateToStudioDocument} from '../hooks/dashboard/useNavigateToStudioDocument'
30
- export {useStudioWorkspacesByResourceId} from '../hooks/dashboard/useStudioWorkspacesByResourceId'
29
+ export {useStudioWorkspacesByProjectIdDataset} from '../hooks/dashboard/useStudioWorkspacesByProjectIdDataset'
31
30
  export {useDatasets} from '../hooks/datasets/useDatasets'
32
31
  export {useApplyDocumentActions} from '../hooks/document/useApplyDocumentActions'
33
32
  export {useDocument} from '../hooks/document/useDocument'
@@ -58,7 +57,7 @@ export {
58
57
  export {useProject} from '../hooks/projects/useProject'
59
58
  export {type ProjectWithoutMembers, useProjects} from '../hooks/projects/useProjects'
60
59
  export {useQuery} from '../hooks/query/useQuery'
61
- export {useUsers, type UseUsersParams, type UseUsersResult} from '../hooks/users/useUsers'
60
+ export {type UsersResult, useUsers} from '../hooks/users/useUsers'
62
61
  export {REACT_SDK_VERSION} from '../version'
63
62
  export {type DatasetsResponse, type SanityProject, type SanityProjectMember} from '@sanity/client'
64
63
  export {type Status as ComlinkStatus} from '@sanity/comlink'
@@ -1,79 +1,103 @@
1
- import * as SanitySDK from '@sanity/sdk'
2
1
  import {render} from '@testing-library/react'
3
- import {type ReactNode} from 'react'
2
+ import React from 'react'
4
3
  import {describe, expect, it, vi} from 'vitest'
5
4
 
6
- import {type SanityProviderProps} from '../context/SanityProvider'
7
5
  import {SDKProvider} from './SDKProvider'
8
6
 
9
- // Mock the SDK module
10
- vi.mock('@sanity/sdk', () => ({
11
- createSanityInstance: vi.fn(() => ({
12
- // Mock return value of createSanityInstance
13
- id: 'mock-instance',
14
- })),
15
- }))
16
-
17
- // Mock the SanityProvider context to verify the instance is passed correctly
18
- vi.mock('../context/SanityProvider', () => ({
19
- SanityProvider: ({children, sanityInstances}: SanityProviderProps) => (
20
- <div data-testid="sanity-provider" data-instances={JSON.stringify(sanityInstances)}>
21
- {children}
22
- </div>
23
- ),
24
- useSanity: vi.fn(),
7
+ // Mock ResourceProvider to test nesting behavior
8
+ vi.mock('../context/ResourceProvider', () => ({
9
+ ResourceProvider: ({
10
+ children,
11
+ ...props
12
+ }: {
13
+ children: React.ReactNode
14
+ projectId?: string
15
+ dataset?: string
16
+ }) => {
17
+ return (
18
+ <div
19
+ data-testid="resource-provider"
20
+ data-config={JSON.stringify({
21
+ projectId: props.projectId,
22
+ dataset: props.dataset,
23
+ })}
24
+ >
25
+ {children}
26
+ </div>
27
+ )
28
+ },
25
29
  }))
26
30
 
27
- // Mock the AuthBoundary component
31
+ // Mock AuthBoundary
28
32
  vi.mock('./auth/AuthBoundary', () => ({
29
- AuthBoundary: ({children}: {children: ReactNode}) => (
30
- <div data-testid="auth-boundary">{children}</div>
31
- ),
33
+ AuthBoundary: ({children}: {children: React.ReactNode}) => {
34
+ return <div data-testid="auth-boundary">{children}</div>
35
+ },
32
36
  }))
33
37
 
34
38
  describe('SDKProvider', () => {
35
- const mockConfig = {
36
- projectId: 'test-project',
37
- dataset: 'test-dataset',
38
- }
39
+ it('renders single ResourceProvider with AuthBoundary for a single config', () => {
40
+ const config = {
41
+ projectId: 'test-project',
42
+ dataset: 'production',
43
+ }
39
44
 
40
- it('creates a Sanity instance with the provided config', () => {
41
- render(
42
- <SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
43
- <div>Test Child</div>
45
+ const {getAllByTestId, getByTestId} = render(
46
+ <SDKProvider config={[config]} fallback={<div>Loading...</div>}>
47
+ <div>Child Content</div>
44
48
  </SDKProvider>,
45
49
  )
46
50
 
47
- expect(SanitySDK.createSanityInstance).toHaveBeenCalledWith(mockConfig)
48
- })
49
-
50
- it('renders children within SanityProvider and AuthBoundary', () => {
51
- const {getByText, getByTestId} = render(
52
- <SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
53
- <div>Test Child</div>
54
- </SDKProvider>,
55
- )
51
+ // Should create a single ResourceProvider
52
+ const providers = getAllByTestId('resource-provider')
53
+ expect(providers.length).toBe(1)
56
54
 
57
- // Verify the component hierarchy
58
- const sanityProvider = getByTestId('sanity-provider')
59
- const authBoundary = getByTestId('auth-boundary')
60
- const childElement = getByText('Test Child')
55
+ // Should create an AuthBoundary inside
56
+ expect(getByTestId('auth-boundary')).toBeInTheDocument()
61
57
 
62
- expect(sanityProvider).toBeInTheDocument()
63
- expect(authBoundary).toBeInTheDocument()
64
- expect(childElement).toBeInTheDocument()
58
+ // Verify provider has the correct config
59
+ expect(JSON.parse(providers[0].getAttribute('data-config') || '{}')).toEqual({
60
+ projectId: 'test-project',
61
+ dataset: 'production',
62
+ })
65
63
  })
66
64
 
67
- it('passes the created Sanity instance to SanityProvider', () => {
68
- const {getByTestId} = render(
69
- <SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
70
- <div>Test Child</div>
65
+ it('renders nested ResourceProviders with AuthBoundary for multiple configs', () => {
66
+ const configs = [
67
+ {
68
+ projectId: 'project-1',
69
+ dataset: 'production',
70
+ },
71
+ {
72
+ projectId: 'project-2',
73
+ dataset: 'staging',
74
+ },
75
+ ]
76
+
77
+ const {getAllByTestId, getByTestId} = render(
78
+ <SDKProvider config={configs} fallback={<div>Loading...</div>}>
79
+ <div>Child Content</div>
71
80
  </SDKProvider>,
72
81
  )
73
82
 
74
- const sanityProvider = getByTestId('sanity-provider')
75
- const passedInstances = JSON.parse(sanityProvider.dataset['instances'] || '[]')
83
+ // Should create two nested ResourceProviders
84
+ const providers = getAllByTestId('resource-provider')
85
+ expect(providers.length).toBe(2)
86
+
87
+ // Should create an AuthBoundary inside the innermost provider
88
+ expect(getByTestId('auth-boundary')).toBeInTheDocument()
89
+
90
+ // Verify each provider has the correct config - order is based on how SDKProvider creates nestings
91
+ // The first provider contains config[1]
92
+ expect(JSON.parse(providers[0].getAttribute('data-config') || '{}')).toEqual({
93
+ projectId: 'project-2',
94
+ dataset: 'staging',
95
+ })
76
96
 
77
- expect(passedInstances).toEqual([{id: 'mock-instance'}])
97
+ // The second provider contains config[0]
98
+ expect(JSON.parse(providers[1].getAttribute('data-config') || '{}')).toEqual({
99
+ projectId: 'project-1',
100
+ dataset: 'production',
101
+ })
78
102
  })
79
103
  })
@@ -1,21 +1,15 @@
1
- import {createSanityInstance, type SanityConfig} from '@sanity/sdk'
2
- import {type ReactElement, type ReactNode, Suspense, useMemo} from 'react'
1
+ import {type SanityConfig} from '@sanity/sdk'
2
+ import {type ReactElement, type ReactNode} from 'react'
3
3
 
4
- import {SanityProvider} from '../context/SanityProvider'
5
- import {AuthBoundary} from './auth/AuthBoundary'
6
-
7
- const DEFAULT_FALLBACK = (
8
- <>
9
- Warning: No fallback provided. Please supply a fallback prop to ensure proper Suspense handling.
10
- </>
11
- )
4
+ import {ResourceProvider} from '../context/ResourceProvider'
5
+ import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
12
6
 
13
7
  /**
14
8
  * @internal
15
9
  */
16
- export interface SDKProviderProps {
10
+ export interface SDKProviderProps extends AuthBoundaryProps {
17
11
  children: ReactNode
18
- sanityConfigs: SanityConfig[]
12
+ config: SanityConfig | SanityConfig[]
19
13
  fallback: ReactNode
20
14
  }
21
15
 
@@ -23,20 +17,31 @@ export interface SDKProviderProps {
23
17
  * @internal
24
18
  *
25
19
  * Top-level context provider that provides access to the Sanity SDK.
20
+ * Creates a hierarchy of ResourceProviders, each providing a SanityInstance that can be
21
+ * accessed by hooks. The first configuration in the array becomes the default instance.
26
22
  */
27
- export function SDKProvider({children, sanityConfigs, fallback}: SDKProviderProps): ReactElement {
28
- const sanityInstances = useMemo(() => {
29
- return sanityConfigs.map((sanityConfig: SanityConfig) => createSanityInstance(sanityConfig))
30
- }, [sanityConfigs])
23
+ export function SDKProvider({
24
+ children,
25
+ config,
26
+ fallback,
27
+ ...props
28
+ }: SDKProviderProps): ReactElement {
29
+ // reverse because we want the first config to be the default, but the
30
+ // ResourceProvider nesting makes the last one the default
31
+ const configs = (Array.isArray(config) ? config : [config]).slice().reverse()
32
+
33
+ // Create a nested structure of ResourceProviders for each config
34
+ const createNestedProviders = (index: number): ReactElement => {
35
+ if (index >= configs.length) {
36
+ return <AuthBoundary {...props}>{children}</AuthBoundary>
37
+ }
38
+
39
+ return (
40
+ <ResourceProvider {...configs[index]} fallback={fallback}>
41
+ {createNestedProviders(index + 1)}
42
+ </ResourceProvider>
43
+ )
44
+ }
31
45
 
32
- return (
33
- <SanityProvider sanityInstances={sanityInstances}>
34
- {/* This Suspense boundary is necessary because some hooks may suspend.
35
- It ensures that the Sanity instance state created above remains stable
36
- before rendering the AuthBoundary and its children. */}
37
- <Suspense fallback={fallback ?? DEFAULT_FALLBACK}>
38
- <AuthBoundary>{children}</AuthBoundary>
39
- </Suspense>
40
- </SanityProvider>
41
- )
46
+ return createNestedProviders(0)
42
47
  }
@@ -3,9 +3,16 @@ import {render, screen} from '@testing-library/react'
3
3
  import {describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {SanityApp} from './SanityApp'
6
+ import {SDKProvider} from './SDKProvider'
6
7
 
7
- vi.mock('@sanity/sdk', async () => {
8
- const actual = await vi.importActual('@sanity/sdk')
8
+ // Mock SDKProvider to verify it's being used correctly
9
+ vi.mock('./SDKProvider', () => ({
10
+ SDKProvider: vi.fn(() => <div data-testid="sdk-provider">SDKProvider</div>),
11
+ }))
12
+
13
+ // Mock useEffect to prevent redirect logic from running in tests
14
+ vi.mock('react', async () => {
15
+ const actual = await vi.importActual('react')
9
16
  return {
10
17
  ...actual,
11
18
  createSanityInstance: vi.fn(() => ({
@@ -36,20 +43,104 @@ vi.mock('../hooks/auth/useAuthState', () => ({
36
43
  }))
37
44
 
38
45
  describe('SanityApp', () => {
39
- const mockSanityConfig: SanityConfig = {
40
- projectId: 'test-project',
41
- dataset: 'test-dataset',
42
- }
46
+ beforeEach(() => {
47
+ vi.clearAllMocks()
48
+ })
49
+
50
+ it('renders SDKProvider with a single config', () => {
51
+ const singleConfig = {
52
+ projectId: 'test-project',
53
+ dataset: 'production',
54
+ }
55
+
56
+ render(
57
+ <SanityApp config={singleConfig} fallback={<div>Loading...</div>}>
58
+ <div>Child Content</div>
59
+ </SanityApp>,
60
+ )
61
+
62
+ // Check that the SDKProvider is rendered
63
+ expect(screen.getByTestId('sdk-provider')).toBeInTheDocument()
64
+
65
+ // Verify SDKProvider was called with the correct props
66
+ const sdkProviderCalls = vi.mocked(SDKProvider).mock.calls
67
+ expect(sdkProviderCalls.length).toBe(1)
68
+
69
+ const [props] = sdkProviderCalls[0]
70
+ const {config} = props
71
+
72
+ // Config is now passed directly as an object for single configs
73
+ expect(config).toEqual(singleConfig)
74
+ expect(props.fallback).toBeTruthy()
75
+ })
76
+
77
+ it('renders SDKProvider with multiple configs in original order', () => {
78
+ const multipleConfigs = [
79
+ {
80
+ projectId: 'project-1',
81
+ dataset: 'production',
82
+ },
83
+ {
84
+ projectId: 'project-2',
85
+ dataset: 'staging',
86
+ },
87
+ {
88
+ projectId: 'project-3',
89
+ dataset: 'development',
90
+ },
91
+ ]
92
+
93
+ render(
94
+ <SanityApp config={multipleConfigs} fallback={<div>Loading...</div>}>
95
+ <div>Child Content</div>
96
+ </SanityApp>,
97
+ )
98
+
99
+ // Check that the SDKProvider is rendered
100
+ expect(screen.getByTestId('sdk-provider')).toBeInTheDocument()
101
+
102
+ // Verify SDKProvider was called with the correct props
103
+ const sdkProviderCalls = vi.mocked(SDKProvider).mock.calls
104
+ expect(sdkProviderCalls.length).toBe(1)
105
+
106
+ const [props] = sdkProviderCalls[0]
107
+ const {config} = props
108
+
109
+ // Config should be passed directly to SDKProvider
110
+ expect(config).toEqual(multipleConfigs)
111
+ })
112
+
113
+ it('supports legacy sanityConfigs prop', () => {
114
+ const legacyConfigs = [
115
+ {
116
+ projectId: 'legacy-project-1',
117
+ dataset: 'production',
118
+ },
119
+ {
120
+ projectId: 'legacy-project-2',
121
+ dataset: 'staging',
122
+ },
123
+ ]
43
124
 
44
- it('renders children correctly', async () => {
45
- const testMessage = 'Test Child Component'
46
125
  render(
47
- <SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
48
- <div>{testMessage}</div>
126
+ // @ts-expect-error purposefully using the deprecated prop
127
+ <SanityApp sanityConfigs={legacyConfigs} fallback={<div>Loading...</div>}>
128
+ <div>Child Content</div>
49
129
  </SanityApp>,
50
130
  )
51
131
 
52
- expect(await screen.findByText(testMessage)).toBeInTheDocument()
132
+ // Check that the SDKProvider is rendered
133
+ expect(screen.getByTestId('sdk-provider')).toBeInTheDocument()
134
+
135
+ // Verify SDKProvider was called with the correct props
136
+ const sdkProviderCalls = vi.mocked(SDKProvider).mock.calls
137
+ expect(sdkProviderCalls.length).toBe(1)
138
+
139
+ const [props] = sdkProviderCalls[0]
140
+ const {config} = props
141
+
142
+ // Config should be passed to SDKProvider in the same order
143
+ expect(config).toEqual(legacyConfigs)
53
144
  })
54
145
 
55
146
  it('handles iframe environment correctly', async () => {
@@ -57,6 +148,11 @@ describe('SanityApp', () => {
57
148
  const originalTop = window.top
58
149
  const originalSelf = window.self
59
150
 
151
+ const mockSanityConfig: SanityConfig = {
152
+ projectId: 'test-project',
153
+ dataset: 'test-dataset',
154
+ }
155
+
60
156
  const mockTop = {}
61
157
  Object.defineProperty(window, 'top', {
62
158
  value: mockTop,
@@ -68,7 +164,7 @@ describe('SanityApp', () => {
68
164
  })
69
165
 
70
166
  render(
71
- <SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
167
+ <SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
72
168
  <div>Test Child</div>
73
169
  </SanityApp>,
74
170
  )
@@ -98,13 +194,18 @@ describe('SanityApp', () => {
98
194
  href: 'http://sanity-test.app',
99
195
  }
100
196
 
197
+ const mockSanityConfig: SanityConfig = {
198
+ projectId: 'test-project',
199
+ dataset: 'test-dataset',
200
+ }
201
+
101
202
  Object.defineProperty(window, 'location', {
102
203
  value: mockLocation,
103
204
  writable: true,
104
205
  })
105
206
 
106
207
  render(
107
- <SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
208
+ <SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
108
209
  <div>Test Child</div>
109
210
  </SanityApp>,
110
211
  )
@@ -113,7 +214,7 @@ describe('SanityApp', () => {
113
214
  await new Promise((resolve) => setTimeout(resolve, 1010))
114
215
 
115
216
  // Add assertions based on your iframe-specific behavior
116
- expect(mockLocation.replace).toHaveBeenCalledWith('https://core.sanity.io')
217
+ expect(mockLocation.replace).toHaveBeenCalledWith('https://sanity.io/welcome')
117
218
 
118
219
  // Clean up the mock
119
220
  Object.defineProperty(window, 'location', {
@@ -125,6 +226,11 @@ describe('SanityApp', () => {
125
226
  it('does not redirect to core if not inside iframe and local url', async () => {
126
227
  const originalLocation = window.location
127
228
 
229
+ const mockSanityConfig: SanityConfig = {
230
+ projectId: 'test-project',
231
+ dataset: 'test-dataset',
232
+ }
233
+
128
234
  const mockLocation = {
129
235
  replace: vi.fn(),
130
236
  href: 'http://localhost:3000',
@@ -136,7 +242,7 @@ describe('SanityApp', () => {
136
242
  })
137
243
 
138
244
  render(
139
- <SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
245
+ <SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
140
246
  <div>Test Child</div>
141
247
  </SanityApp>,
142
248
  )
@@ -8,12 +8,14 @@ import {isInIframe, isLocalUrl} from './utils'
8
8
  * @public
9
9
  */
10
10
  export interface SanityAppProps {
11
- sanityConfigs: SanityConfig[]
11
+ config: SanityConfig | SanityConfig[]
12
+ /** @deprecated use the `config` prop instead. */
13
+ sanityConfigs?: SanityConfig[]
12
14
  children: React.ReactNode
13
15
  fallback: React.ReactNode
14
16
  }
15
17
 
16
- const CORE_URL = 'https://core.sanity.io'
18
+ const REDIRECT_URL = 'https://sanity.io/welcome'
17
19
 
18
20
  /**
19
21
  * @public
@@ -22,6 +24,9 @@ const CORE_URL = 'https://core.sanity.io'
22
24
  * as well as application context and state which is used by the Sanity React hooks. Your application
23
25
  * must be wrapped with the SanityApp component to function properly.
24
26
  *
27
+ * SanityApp creates a hierarchy of ResourceProviders, each providing a SanityInstance that can be
28
+ * accessed by hooks. The first configuration in the array becomes the default instance.
29
+ *
25
30
  * @param props - Your Sanity configuration and the React children to render
26
31
  * @returns Your Sanity application, integrated with your Sanity configuration and application context
27
32
  *
@@ -32,16 +37,14 @@ const CORE_URL = 'https://core.sanity.io'
32
37
  * import MyAppRoot from './Root'
33
38
  *
34
39
  * // Single project configuration
35
- * const mySanityConfigs = [
36
- * {
37
- * projectId: 'my-project-id',
38
- * dataset: 'production',
39
- * },
40
- * ]
40
+ * const mySanityConfig = {
41
+ * projectId: 'my-project-id',
42
+ * dataset: 'production',
43
+ * }
41
44
  *
42
45
  * // Or multiple project configurations
43
46
  * const multipleConfigs = [
44
- * // Configuration for your main project. This will be used as the default project for all hooks if no resource ID override is provided.
47
+ * // Configuration for your main project. This will be used as the default project for hooks.
45
48
  * {
46
49
  * projectId: 'marketing-website-project',
47
50
  * dataset: 'production',
@@ -60,14 +63,22 @@ const CORE_URL = 'https://core.sanity.io'
60
63
  *
61
64
  * export default function MyApp() {
62
65
  * return (
63
- * <SanityApp sanityConfigs={mySanityConfigs}>
66
+ * <SanityApp config={mySanityConfig} fallback={<LoadingSpinner />}>
64
67
  * <MyAppRoot />
65
68
  * </SanityApp>
66
69
  * )
67
70
  * }
68
71
  * ```
69
72
  */
70
- export function SanityApp({sanityConfigs, children, fallback}: SanityAppProps): ReactElement {
73
+ export function SanityApp({
74
+ children,
75
+ fallback,
76
+ config,
77
+ sanityConfigs,
78
+ ...props
79
+ }: SanityAppProps): ReactElement {
80
+ const configs = config ?? sanityConfigs ?? []
81
+
71
82
  useEffect(() => {
72
83
  let timeout: NodeJS.Timeout | undefined
73
84
 
@@ -75,15 +86,15 @@ export function SanityApp({sanityConfigs, children, fallback}: SanityAppProps):
75
86
  // If the app is not running in an iframe and is not a local url, redirect to core.
76
87
  timeout = setTimeout(() => {
77
88
  // eslint-disable-next-line no-console
78
- console.warn('Redirecting to core', CORE_URL)
79
- window.location.replace(CORE_URL)
89
+ console.warn('Redirecting to core', REDIRECT_URL)
90
+ window.location.replace(REDIRECT_URL)
80
91
  }, 1000)
81
92
  }
82
93
  return () => clearTimeout(timeout)
83
- }, [sanityConfigs])
94
+ }, [])
84
95
 
85
96
  return (
86
- <SDKProvider sanityConfigs={sanityConfigs} fallback={fallback}>
97
+ <SDKProvider {...props} fallback={fallback} config={configs}>
87
98
  {children}
88
99
  </SDKProvider>
89
100
  )