@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2

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 (168) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +2488 -2019
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4435 -1529
  5. package/dist/index.js.map +1 -1
  6. package/package.json +55 -59
  7. package/src/_exports/index.ts +2 -4
  8. package/src/_exports/sdk-react.ts +30 -18
  9. package/src/components/SDKProvider.test.tsx +173 -32
  10. package/src/components/SDKProvider.tsx +87 -28
  11. package/src/components/SanityApp.test.tsx +68 -12
  12. package/src/components/SanityApp.tsx +93 -87
  13. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  14. package/src/components/auth/AuthBoundary.test.tsx +7 -7
  15. package/src/components/auth/AuthBoundary.tsx +29 -6
  16. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  17. package/src/components/auth/LoginError.test.tsx +191 -5
  18. package/src/components/auth/LoginError.tsx +104 -61
  19. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  20. package/src/components/errors/ChunkLoadError.tsx +56 -0
  21. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  22. package/src/components/errors/chunkReloadStorage.ts +57 -0
  23. package/src/config/handles.ts +29 -27
  24. package/src/constants.ts +5 -0
  25. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  26. package/src/context/ComlinkTokenRefresh.tsx +3 -5
  27. package/src/context/DefaultResourceContext.ts +3 -3
  28. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  29. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  30. package/src/context/PerspectiveContext.ts +3 -3
  31. package/src/context/ProjectContext.ts +15 -0
  32. package/src/context/ResourceProvider.test.tsx +93 -90
  33. package/src/context/ResourceProvider.tsx +62 -93
  34. package/src/context/SDKStudioContext.test.tsx +28 -33
  35. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  36. package/src/context/SanityInstanceProvider.tsx +71 -0
  37. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  38. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  39. package/src/context/renderSanityApp.test.tsx +151 -49
  40. package/src/context/renderSanityApp.tsx +12 -8
  41. package/src/context/workbenchToken.ts +63 -0
  42. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  43. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  44. package/src/hooks/access/useCheckPermissions.ts +24 -0
  45. package/src/hooks/agent/agentActions.test.tsx +2 -2
  46. package/src/hooks/agent/agentActions.ts +52 -72
  47. package/src/hooks/agent/useAgentResourceContext.ts +1 -1
  48. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  49. package/src/hooks/applications/useApplication.ts +22 -0
  50. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  51. package/src/hooks/applications/useApplications.ts +25 -0
  52. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  53. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  54. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  55. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  56. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  57. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  58. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  59. package/src/hooks/client/useClient.test.tsx +1 -4
  60. package/src/hooks/client/useClient.ts +1 -0
  61. package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
  62. package/src/hooks/comlink/useFrameConnection.ts +1 -1
  63. package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
  64. package/src/hooks/comlink/useWindowConnection.ts +2 -3
  65. package/src/hooks/comments/useCommentActions.test.tsx +150 -0
  66. package/src/hooks/comments/useCommentActions.ts +109 -0
  67. package/src/hooks/comments/useCommentList.ts +79 -0
  68. package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
  69. package/src/hooks/comments/useCommentThreads.ts +73 -0
  70. package/src/hooks/comments/useComments.test.tsx +242 -0
  71. package/src/hooks/comments/useComments.ts +59 -0
  72. package/src/hooks/context/useResource.test.tsx +32 -0
  73. package/src/hooks/context/useResource.ts +24 -0
  74. package/src/hooks/context/useSanityInstance.test.tsx +18 -1
  75. package/src/hooks/context/useSanityInstance.ts +6 -6
  76. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  77. package/src/hooks/dashboard/useManageFavorite.ts +11 -17
  78. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  79. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  80. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  81. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
  82. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  83. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  84. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  85. package/src/hooks/datasets/useDatasets.ts +33 -13
  86. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  87. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  88. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  89. package/src/hooks/document/useCreateDocument.ts +117 -0
  90. package/src/hooks/document/useDocument.test.tsx +37 -8
  91. package/src/hooks/document/useDocument.ts +131 -66
  92. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  93. package/src/hooks/document/useDocumentEvent.ts +16 -10
  94. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  95. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  96. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  97. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  98. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  99. package/src/hooks/document/useEditDocument.ts +163 -17
  100. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  101. package/src/hooks/documents/useDocuments.ts +31 -20
  102. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  103. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  104. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  105. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  106. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  107. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  108. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  109. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  110. package/src/hooks/helpers/useApplyActions.ts +68 -0
  111. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
  112. package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
  113. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  114. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  115. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  116. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  117. package/src/hooks/installations/useInstallation.ts +22 -0
  118. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  119. package/src/hooks/installations/useInstallations.ts +25 -0
  120. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  121. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  122. package/src/hooks/organizations/useOrganization.ts +34 -0
  123. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  124. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  125. package/src/hooks/organizations/useOrganizations.ts +39 -0
  126. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  127. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
  128. package/src/hooks/presence/usePresence.test.tsx +0 -15
  129. package/src/hooks/presence/usePresence.ts +22 -20
  130. package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
  131. package/src/hooks/presence/usePresenceForDocument.ts +104 -0
  132. package/src/hooks/presence/useReportPresence.test.tsx +202 -0
  133. package/src/hooks/presence/useReportPresence.ts +170 -0
  134. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  135. package/src/hooks/preview/useDocumentPreview.tsx +12 -14
  136. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  137. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  138. package/src/hooks/projects/useProject.test-d.ts +56 -0
  139. package/src/hooks/projects/useProject.test.tsx +120 -0
  140. package/src/hooks/projects/useProject.ts +42 -43
  141. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  142. package/src/hooks/projects/useProjects.test.ts +45 -98
  143. package/src/hooks/projects/useProjects.ts +17 -32
  144. package/src/hooks/query/useQuery.test.tsx +6 -5
  145. package/src/hooks/query/useQuery.ts +94 -17
  146. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  147. package/src/hooks/releases/useActiveReleases.ts +28 -24
  148. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  149. package/src/hooks/releases/useAllReleases.ts +62 -0
  150. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  151. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  152. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  153. package/src/hooks/releases/usePerspective.ts +13 -17
  154. package/src/hooks/users/useUser.test.tsx +3 -9
  155. package/src/hooks/users/useUser.ts +4 -8
  156. package/src/hooks/users/useUsers.test.tsx +103 -7
  157. package/src/hooks/users/useUsers.ts +38 -5
  158. package/src/utils/resolveOrgResources.test.ts +111 -0
  159. package/src/utils/resolveOrgResources.ts +69 -0
  160. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  161. package/src/hooks/context/useDefaultResource.ts +0 -30
  162. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  163. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  164. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  165. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  166. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  167. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  168. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -0,0 +1,71 @@
1
+ import {type SanityInstance} from '@sanity/sdk'
2
+ import {Suspense} from 'react'
3
+
4
+ import {SanityInstanceContext} from './SanityInstanceContext'
5
+
6
+ /**
7
+ * Props for the SanityInstanceProvider component
8
+ * @public
9
+ */
10
+ export interface SanityInstanceProviderProps {
11
+ /**
12
+ * A pre-created SanityInstance to provide to child components.
13
+ * The caller owns the instance lifecycle — SanityInstanceProvider
14
+ * will not dispose it on unmount.
15
+ */
16
+ instance: SanityInstance
17
+ /**
18
+ * React node to show while content is loading.
19
+ * Used as the fallback for the internal Suspense boundary.
20
+ */
21
+ fallback: React.ReactNode
22
+ children: React.ReactNode
23
+ }
24
+
25
+ /**
26
+ * Provides an externally-created Sanity instance to child components through React Context.
27
+ *
28
+ * @internal
29
+ *
30
+ * @remarks
31
+ * Unlike {@link ResourceProvider}, this component does not create or dispose a SanityInstance.
32
+ * The caller is responsible for creating the instance via `createSanityInstance` and disposing
33
+ * it when appropriate. This is useful when a non-React system layer (e.g. a state machine)
34
+ * owns the instance and the React tree should consume it without managing its lifecycle.
35
+ *
36
+ * All SDK hooks (`useSanityInstance`, `useDocuments`, etc.) will read from the provided instance.
37
+ *
38
+ * @example Providing a pre-created instance
39
+ * ```tsx
40
+ * import { createSanityInstance, type SanityConfig } from '@sanity/sdk'
41
+ * import { SanityInstanceProvider } from '@sanity/sdk-react'
42
+ *
43
+ * const config: SanityConfig = {
44
+ * projectId: 'my-project-id',
45
+ * dataset: 'production',
46
+ * }
47
+ *
48
+ * const instance = createSanityInstance(config)
49
+ *
50
+ * function App() {
51
+ * return (
52
+ * <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
53
+ * <MyApp />
54
+ * </SanityInstanceProvider>
55
+ * )
56
+ * }
57
+ * ```
58
+ *
59
+ * @category Components
60
+ */
61
+ export function SanityInstanceProvider({
62
+ instance,
63
+ fallback,
64
+ children,
65
+ }: SanityInstanceProviderProps): React.ReactNode {
66
+ return (
67
+ <SanityInstanceContext.Provider value={instance}>
68
+ <Suspense fallback={fallback}>{children}</Suspense>
69
+ </SanityInstanceContext.Provider>
70
+ )
71
+ }
@@ -0,0 +1,106 @@
1
+ import {AuthStateType, setAuthToken} from '@sanity/sdk'
2
+ import {act, render} from '@testing-library/react'
3
+ import {of} from 'rxjs'
4
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
5
+
6
+ import {useAuthState} from '../hooks/auth/useAuthState'
7
+ import {ResourceProvider} from './ResourceProvider'
8
+ import {
9
+ isWorkbenchEnvironment,
10
+ observeWorkbenchToken,
11
+ refreshWorkbenchToken,
12
+ } from './workbenchToken'
13
+ import {WorkbenchTokenRefreshProvider} from './WorkbenchTokenRefresh'
14
+
15
+ vi.mock('@sanity/sdk', async () => {
16
+ const actual = await vi.importActual('@sanity/sdk')
17
+ return {
18
+ ...actual,
19
+ setAuthToken: vi.fn(),
20
+ }
21
+ })
22
+
23
+ vi.mock('../hooks/auth/useAuthState', () => ({
24
+ useAuthState: vi.fn(),
25
+ }))
26
+
27
+ vi.mock('./workbenchToken', () => ({
28
+ isWorkbenchEnvironment: vi.fn(() => false),
29
+ observeWorkbenchToken: vi.fn(() => undefined),
30
+ refreshWorkbenchToken: vi.fn(),
31
+ }))
32
+
33
+ const mockSetAuthToken = setAuthToken as Mock
34
+ const mockUseAuthState = useAuthState as Mock
35
+ const mockIsWorkbenchEnvironment = isWorkbenchEnvironment as Mock
36
+ const mockObserveWorkbenchToken = observeWorkbenchToken as Mock
37
+ const mockRefreshWorkbenchToken = refreshWorkbenchToken as Mock
38
+
39
+ const renderProvider = () =>
40
+ render(
41
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
42
+ <WorkbenchTokenRefreshProvider>
43
+ <div>Test</div>
44
+ </WorkbenchTokenRefreshProvider>
45
+ </ResourceProvider>,
46
+ )
47
+
48
+ describe('WorkbenchTokenRefreshProvider', () => {
49
+ beforeEach(() => {
50
+ mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
51
+ })
52
+
53
+ afterEach(() => {
54
+ vi.clearAllMocks()
55
+ })
56
+
57
+ describe('when not in the workbench', () => {
58
+ it('does not subscribe to the OS token', () => {
59
+ mockIsWorkbenchEnvironment.mockReturnValue(false)
60
+
61
+ act(() => {
62
+ renderProvider()
63
+ })
64
+
65
+ expect(mockSetAuthToken).not.toHaveBeenCalled()
66
+ })
67
+ })
68
+
69
+ describe('when in the workbench', () => {
70
+ beforeEach(() => {
71
+ mockIsWorkbenchEnvironment.mockReturnValue(true)
72
+ })
73
+
74
+ it('mirrors the OS token into the auth store', () => {
75
+ mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
76
+
77
+ act(() => {
78
+ renderProvider()
79
+ })
80
+
81
+ expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), 'workbench-token')
82
+ })
83
+
84
+ it('asks the OS to reissue the token on a 401', () => {
85
+ mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
86
+
87
+ const {rerender} = renderProvider()
88
+
89
+ mockUseAuthState.mockReturnValue({
90
+ type: AuthStateType.ERROR,
91
+ error: {statusCode: 401, message: 'Unauthorized'},
92
+ })
93
+ act(() => {
94
+ rerender(
95
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
96
+ <WorkbenchTokenRefreshProvider>
97
+ <div>Test</div>
98
+ </WorkbenchTokenRefreshProvider>
99
+ </ResourceProvider>,
100
+ )
101
+ })
102
+
103
+ expect(mockRefreshWorkbenchToken).toHaveBeenCalledTimes(1)
104
+ })
105
+ })
106
+ })
@@ -0,0 +1,61 @@
1
+ import {type ClientError} from '@sanity/client'
2
+ import {AuthStateType, setAuthToken} from '@sanity/sdk'
3
+ import React, {type PropsWithChildren, useEffect, useRef} from 'react'
4
+
5
+ import {useAuthState} from '../hooks/auth/useAuthState'
6
+ import {useSanityInstance} from '../hooks/context/useSanityInstance'
7
+ import {
8
+ isWorkbenchEnvironment,
9
+ observeWorkbenchToken,
10
+ refreshWorkbenchToken,
11
+ } from './workbenchToken'
12
+
13
+ /**
14
+ * Keeps the SDK auth token in sync with the workbench "OS".
15
+ *
16
+ * When running as a federated remote inside the workbench the OS owns the
17
+ * session, so we subscribe to its `auth.token` stream and mirror each value into
18
+ * the auth store — a token logs us in, `null` logs us out, and later OS
19
+ * sign-in/out propagates automatically. When a request is rejected with a 401
20
+ * (the token expired), we ask the OS to reissue rather than tearing the session
21
+ * down; the new token arrives back through the same subscription.
22
+ */
23
+ function WorkbenchTokenRefresh({children}: PropsWithChildren) {
24
+ const instance = useSanityInstance()
25
+ const authState = useAuthState()
26
+ const processed401ErrorRef = useRef<unknown | null>(null)
27
+
28
+ useEffect(() => {
29
+ const token$ = observeWorkbenchToken()
30
+ if (!token$) return undefined
31
+ const subscription = token$.subscribe((token) => setAuthToken(instance, token))
32
+ return () => subscription.unsubscribe()
33
+ }, [instance])
34
+
35
+ useEffect(() => {
36
+ const has401Error =
37
+ authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401
38
+
39
+ if (has401Error && processed401ErrorRef.current !== authState.error) {
40
+ processed401ErrorRef.current = authState.error
41
+ refreshWorkbenchToken()
42
+ } else if (!has401Error) {
43
+ processed401ErrorRef.current = null
44
+ }
45
+ }, [authState])
46
+
47
+ return children
48
+ }
49
+
50
+ /**
51
+ * Provides workbench OS token refresh. No-op outside the workbench, where the
52
+ * app uses its normal auth flow.
53
+ * @public
54
+ */
55
+ export const WorkbenchTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
56
+ if (isWorkbenchEnvironment()) {
57
+ return <WorkbenchTokenRefresh>{children}</WorkbenchTokenRefresh>
58
+ }
59
+
60
+ return children
61
+ }
@@ -1,4 +1,4 @@
1
- import {type DocumentResource} from '@sanity/sdk'
1
+ import {type SanityConfig} from '@sanity/sdk'
2
2
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
3
 
4
4
  import {SanityApp} from '../components/SanityApp'
@@ -26,7 +26,6 @@ vi.mock('react-dom/client', () => ({
26
26
 
27
27
  describe('renderSanityApp', () => {
28
28
  let rootElement: HTMLElement | null
29
- let namedResources: Record<string, DocumentResource>
30
29
 
31
30
  beforeEach(() => {
32
31
  vi.clearAllMocks()
@@ -35,9 +34,6 @@ describe('renderSanityApp', () => {
35
34
  mockCreateRoot.mockClear()
36
35
  rootElement = document.createElement('div')
37
36
  document.body.appendChild(rootElement)
38
- namedResources = {
39
- default: {projectId: 'test-project', dataset: 'production'},
40
- }
41
37
  })
42
38
 
43
39
  afterEach(() => {
@@ -48,44 +44,53 @@ describe('renderSanityApp', () => {
48
44
  })
49
45
 
50
46
  it('throws error when rootElement is null', () => {
51
- expect(() => renderSanityApp(null, namedResources, {}, <div>Test</div>)).toThrowError(
47
+ const namedSources = {
48
+ main: {projectId: 'test-project', dataset: 'production'},
49
+ }
50
+
51
+ expect(() => renderSanityApp(null, namedSources, {}, <div>Test</div>)).toThrowError(
52
52
  'Missing root element to mount application into',
53
53
  )
54
54
  })
55
55
 
56
56
  it('creates root with the provided element', () => {
57
- renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
57
+ const namedSources = {
58
+ main: {projectId: 'test-project', dataset: 'production'},
59
+ }
60
+
61
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
58
62
 
59
63
  expect(mockCreateRoot).toHaveBeenCalledWith(rootElement)
60
64
  expect(mockCreateRoot).toHaveBeenCalledTimes(1)
61
65
  })
62
66
 
63
- it('merges namedResources into a single config and resources map', () => {
64
- namedResources = {
65
- default: {projectId: 'project-1', dataset: 'production'},
67
+ it('converts namedSources object to array of configs', () => {
68
+ const namedSources = {
69
+ main: {projectId: 'project-1', dataset: 'production'},
66
70
  secondary: {projectId: 'project-2', dataset: 'staging'},
67
71
  }
68
72
 
69
- renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
73
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
70
74
 
71
75
  expect(mockRender).toHaveBeenCalledTimes(1)
72
76
  const renderCall = mockRender.mock.calls[0][0]
73
77
  expect(renderCall).toBeDefined()
74
78
 
79
+ // The renderCall is the SanityApp component directly when not using StrictMode
75
80
  expect(renderCall.type).toBe(SanityApp)
76
- // config has no resource fields
77
- expect(renderCall.props.config).toEqual(undefined)
78
- // resources map is populated from named entries
79
- expect(renderCall.props.resources).toEqual({
80
- default: {projectId: 'project-1', dataset: 'production'},
81
- secondary: {projectId: 'project-2', dataset: 'staging'},
82
- })
81
+ expect(renderCall.props.config).toEqual([
82
+ {projectId: 'project-1', dataset: 'production'},
83
+ {projectId: 'project-2', dataset: 'staging'},
84
+ ])
83
85
  })
84
86
 
85
87
  it('renders without StrictMode when reactStrictMode is false', () => {
88
+ const namedSources = {
89
+ main: {projectId: 'test-project', dataset: 'production'},
90
+ }
86
91
  const children = <div>Test Children</div>
87
92
 
88
- renderSanityApp(rootElement, namedResources, {reactStrictMode: false}, children)
93
+ renderSanityApp(rootElement, namedSources, {reactStrictMode: false}, children)
89
94
 
90
95
  expect(mockRender).toHaveBeenCalledTimes(1)
91
96
  const renderCall = mockRender.mock.calls[0][0]
@@ -96,9 +101,12 @@ describe('renderSanityApp', () => {
96
101
  })
97
102
 
98
103
  it('renders without StrictMode by default', () => {
104
+ const namedSources = {
105
+ main: {projectId: 'test-project', dataset: 'production'},
106
+ }
99
107
  const children = <div>Test Children</div>
100
108
 
101
- renderSanityApp(rootElement, namedResources, {}, children)
109
+ renderSanityApp(rootElement, namedSources, {}, children)
102
110
 
103
111
  expect(mockRender).toHaveBeenCalledTimes(1)
104
112
  const renderCall = mockRender.mock.calls[0][0]
@@ -109,14 +117,17 @@ describe('renderSanityApp', () => {
109
117
  })
110
118
 
111
119
  it('renders with StrictMode when reactStrictMode is true', () => {
120
+ const namedSources = {
121
+ main: {projectId: 'test-project', dataset: 'production'},
122
+ }
112
123
  const children = <div>Test Children</div>
113
124
 
114
- renderSanityApp(rootElement, namedResources, {reactStrictMode: true}, children)
125
+ renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
115
126
 
116
127
  expect(mockRender).toHaveBeenCalledTimes(1)
117
128
  const renderCall = mockRender.mock.calls[0][0]
118
129
 
119
- // Should have StrictMode wrapper (StrictMode is a Symbol)
130
+ // Should have StrictMode wrapper (StrictMode is a Symbol in React 18)
120
131
  expect(renderCall.type).toBeDefined()
121
132
  expect(renderCall.type.toString()).toContain('Symbol')
122
133
  const strictModeChild = renderCall.props.children
@@ -125,7 +136,11 @@ describe('renderSanityApp', () => {
125
136
  })
126
137
 
127
138
  it('passes loading fallback to SanityApp', () => {
128
- renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
139
+ const namedSources = {
140
+ main: {projectId: 'test-project', dataset: 'production'},
141
+ }
142
+
143
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
129
144
 
130
145
  expect(mockRender).toHaveBeenCalledTimes(1)
131
146
  const renderCall = mockRender.mock.calls[0][0]
@@ -136,51 +151,103 @@ describe('renderSanityApp', () => {
136
151
  })
137
152
 
138
153
  it('returns an unmount function', () => {
139
- const unmount = renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
154
+ const namedSources = {
155
+ main: {projectId: 'test-project', dataset: 'production'},
156
+ }
157
+
158
+ const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
140
159
 
141
160
  expect(typeof unmount).toBe('function')
142
161
  })
143
162
 
144
163
  it('calls root.unmount when unmount function is invoked', () => {
145
- const unmount = renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
164
+ const namedSources = {
165
+ main: {projectId: 'test-project', dataset: 'production'},
166
+ }
167
+
168
+ const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
146
169
 
147
170
  expect(mockUnmount).not.toHaveBeenCalled()
148
171
  unmount()
149
172
  expect(mockUnmount).toHaveBeenCalledTimes(1)
150
173
  })
151
174
 
152
- it('handles empty namedResources object', () => {
153
- namedResources = {}
175
+ it('handles empty namedSources object', () => {
176
+ const namedSources = {}
154
177
 
155
- renderSanityApp(rootElement, namedResources, {}, <div>Test</div>)
178
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
156
179
 
157
180
  expect(mockRender).toHaveBeenCalledTimes(1)
158
181
  const renderCall = mockRender.mock.calls[0][0]
159
182
  const sanityAppElement = renderCall
160
183
 
161
184
  expect(sanityAppElement.type).toBe(SanityApp)
162
- expect(sanityAppElement.props.config).toEqual(undefined)
163
- expect(sanityAppElement.props.resources).toEqual({})
185
+ expect(sanityAppElement.props.config).toEqual([])
164
186
  })
165
187
 
166
188
  it('handles single namedSource', () => {
167
- const namedResource = {
168
- default: {projectId: 'test-project', dataset: 'production'},
189
+ const namedSources = {
190
+ main: {projectId: 'test-project', dataset: 'production'},
169
191
  }
170
- renderSanityApp(rootElement, namedResource, {}, <div>Test</div>)
192
+
193
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
171
194
 
172
195
  expect(mockRender).toHaveBeenCalledTimes(1)
173
196
  const renderCall = mockRender.mock.calls[0][0]
174
197
  const sanityAppElement = renderCall
175
198
 
176
199
  expect(sanityAppElement.type).toBe(SanityApp)
177
- expect(sanityAppElement.props.config).toEqual(undefined)
178
- expect(sanityAppElement.props.resources).toEqual({
179
- default: {projectId: 'test-project', dataset: 'production'},
180
- })
200
+ expect(sanityAppElement.props.config).toEqual([
201
+ {projectId: 'test-project', dataset: 'production'},
202
+ ])
203
+ })
204
+
205
+ it('handles multiple namedSources', () => {
206
+ const namedSources = {
207
+ main: {projectId: 'project-1', dataset: 'production'},
208
+ blog: {projectId: 'project-2', dataset: 'staging'},
209
+ ecommerce: {projectId: 'project-3', dataset: 'development'},
210
+ }
211
+
212
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
213
+
214
+ expect(mockRender).toHaveBeenCalledTimes(1)
215
+ const renderCall = mockRender.mock.calls[0][0]
216
+ const sanityAppElement = renderCall
217
+
218
+ expect(sanityAppElement.type).toBe(SanityApp)
219
+ expect(sanityAppElement.props.config).toHaveLength(3)
220
+ expect(sanityAppElement.props.config).toEqual([
221
+ {projectId: 'project-1', dataset: 'production'},
222
+ {projectId: 'project-2', dataset: 'staging'},
223
+ {projectId: 'project-3', dataset: 'development'},
224
+ ])
225
+ })
226
+
227
+ it('preserves order of namedSources in config array', () => {
228
+ const namedSources = {
229
+ z: {projectId: 'project-z', dataset: 'z-dataset'},
230
+ a: {projectId: 'project-a', dataset: 'a-dataset'},
231
+ m: {projectId: 'project-m', dataset: 'm-dataset'},
232
+ }
233
+
234
+ renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
235
+
236
+ const renderCall = mockRender.mock.calls[0][0]
237
+ const sanityAppElement = renderCall
238
+
239
+ // Object.values preserves insertion order in modern JS
240
+ expect(sanityAppElement.props.config).toEqual([
241
+ {projectId: 'project-z', dataset: 'z-dataset'},
242
+ {projectId: 'project-a', dataset: 'a-dataset'},
243
+ {projectId: 'project-m', dataset: 'm-dataset'},
244
+ ])
181
245
  })
182
246
 
183
247
  it('passes children to SanityApp', () => {
248
+ const namedSources = {
249
+ main: {projectId: 'test-project', dataset: 'production'},
250
+ }
184
251
  const children = (
185
252
  <div>
186
253
  <h1>Test App</h1>
@@ -188,7 +255,7 @@ describe('renderSanityApp', () => {
188
255
  </div>
189
256
  )
190
257
 
191
- renderSanityApp(rootElement, namedResources, {}, children)
258
+ renderSanityApp(rootElement, namedSources, {}, children)
192
259
 
193
260
  const renderCall = mockRender.mock.calls[0][0]
194
261
  const sanityAppElement = renderCall
@@ -197,8 +264,12 @@ describe('renderSanityApp', () => {
197
264
  })
198
265
 
199
266
  it('works with different types of children', () => {
267
+ const namedSources = {
268
+ main: {projectId: 'test-project', dataset: 'production'},
269
+ }
270
+
200
271
  // Test with string children
201
- renderSanityApp(rootElement, namedResources, {}, 'String child')
272
+ renderSanityApp(rootElement, namedSources, {}, 'String child')
202
273
 
203
274
  let renderCall = mockRender.mock.calls[0][0]
204
275
  let sanityAppElement = renderCall
@@ -207,7 +278,7 @@ describe('renderSanityApp', () => {
207
278
 
208
279
  // Test with null children
209
280
  mockRender.mockClear()
210
- renderSanityApp(rootElement, namedResources, {}, null)
281
+ renderSanityApp(rootElement, namedSources, {}, null)
211
282
 
212
283
  renderCall = mockRender.mock.calls[0][0]
213
284
  sanityAppElement = renderCall
@@ -217,7 +288,7 @@ describe('renderSanityApp', () => {
217
288
  // Test with array of children
218
289
  mockRender.mockClear()
219
290
  const arrayChildren = [<div key="1">Child 1</div>, <div key="2">Child 2</div>]
220
- renderSanityApp(rootElement, namedResources, {}, arrayChildren)
291
+ renderSanityApp(rootElement, namedSources, {}, arrayChildren)
221
292
 
222
293
  renderCall = mockRender.mock.calls[0][0]
223
294
  sanityAppElement = renderCall
@@ -225,19 +296,50 @@ describe('renderSanityApp', () => {
225
296
  expect(sanityAppElement.props.children).toEqual(arrayChildren)
226
297
  })
227
298
 
299
+ it('integrates with StrictMode and passes all props correctly', () => {
300
+ const namedSources = {
301
+ main: {
302
+ projectId: 'test-project',
303
+ dataset: 'production',
304
+ apiVersion: '2023-01-01',
305
+ } as SanityConfig,
306
+ secondary: {
307
+ projectId: 'test-project-2',
308
+ dataset: 'staging',
309
+ } as SanityConfig,
310
+ }
311
+ const children = <div>App Content</div>
312
+
313
+ renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
314
+
315
+ const renderCall = mockRender.mock.calls[0][0]
316
+
317
+ // Verify StrictMode wrapper (StrictMode is a Symbol in React 18)
318
+ expect(renderCall.type).toBeDefined()
319
+ expect(renderCall.type.toString()).toContain('Symbol')
320
+
321
+ // Verify SanityApp is inside StrictMode
322
+ const strictModeChild = renderCall.props.children
323
+ expect(strictModeChild.type).toBe(SanityApp)
324
+
325
+ // Verify all props are passed correctly
326
+ expect(strictModeChild.props.config).toEqual([
327
+ {projectId: 'test-project', dataset: 'production', apiVersion: '2023-01-01'},
328
+ {projectId: 'test-project-2', dataset: 'staging'},
329
+ ])
330
+ expect(strictModeChild.props.fallback).toEqual(<div>Loading...</div>)
331
+ expect(strictModeChild.props.children).toEqual(children)
332
+ })
333
+
228
334
  it('can be called multiple times with different roots', () => {
229
335
  const rootElement2 = document.createElement('div')
230
336
  document.body.appendChild(rootElement2)
231
337
 
232
- const namedResources1 = {
233
- default: {projectId: 'project-1', dataset: 'production'},
234
- }
235
- const namedResources2 = {
236
- default: {projectId: 'project-2', dataset: 'staging'},
237
- }
338
+ const namedSources1 = {main: {projectId: 'project-1', dataset: 'production'}}
339
+ const namedSources2 = {main: {projectId: 'project-2', dataset: 'staging'}}
238
340
 
239
- const unmount1 = renderSanityApp(rootElement, namedResources1, {}, <div>App 1</div>)
240
- const unmount2 = renderSanityApp(rootElement2, namedResources2, {}, <div>App 2</div>)
341
+ const unmount1 = renderSanityApp(rootElement, namedSources1, {}, <div>App 1</div>)
342
+ const unmount2 = renderSanityApp(rootElement2, namedSources2, {}, <div>App 2</div>)
241
343
 
242
344
  expect(mockCreateRoot).toHaveBeenCalledTimes(2)
243
345
  expect(mockCreateRoot).toHaveBeenNthCalledWith(1, rootElement)
@@ -1,4 +1,4 @@
1
- import {type DocumentResource} from '@sanity/sdk'
1
+ import {type SanityConfig} from '@sanity/sdk'
2
2
  import {StrictMode} from 'react'
3
3
  import {createRoot} from 'react-dom/client'
4
4
 
@@ -8,14 +8,16 @@ interface RenderSanitySDKAppOptions {
8
8
  reactStrictMode?: boolean
9
9
  }
10
10
 
11
- interface NamedResources {
12
- [key: string]: DocumentResource
11
+ /** In-flight CLI PR is using named sources since it's aspirational.
12
+ * We can transform the shape in this function until it's finalized.
13
+ */
14
+ interface NamedSources {
15
+ [key: string]: SanityConfig
13
16
  }
14
-
15
17
  /** @internal */
16
18
  export function renderSanityApp(
17
19
  rootElement: HTMLElement | null,
18
- namedResources: NamedResources,
20
+ namedSources: NamedSources,
19
21
  options: RenderSanitySDKAppOptions,
20
22
  children: React.ReactNode,
21
23
  ): () => void {
@@ -25,16 +27,18 @@ export function renderSanityApp(
25
27
  const {reactStrictMode = false} = options
26
28
 
27
29
  const root = createRoot(rootElement)
30
+ const config = Object.values(namedSources)
31
+
28
32
  root.render(
29
33
  reactStrictMode ? (
30
34
  <StrictMode>
31
- {/* TODO: we should find some way to pass top-level config, like auth, from a flatfile */}
32
- <SanityApp resources={namedResources} fallback={<div>Loading...</div>}>
35
+ {/* TODO: think about a loading component we want to be "universal" */}
36
+ <SanityApp config={config} fallback={<div>Loading...</div>}>
33
37
  {children}
34
38
  </SanityApp>
35
39
  </StrictMode>
36
40
  ) : (
37
- <SanityApp resources={namedResources} fallback={<div>Loading...</div>}>
41
+ <SanityApp config={config} fallback={<div>Loading...</div>}>
38
42
  {children}
39
43
  </SanityApp>
40
44
  ),