sanity-plugin-internationalized-array 0.0.7 → 1.1.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 (40) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +82 -32
  3. package/lib/cjs/index.js +487 -0
  4. package/lib/cjs/index.js.map +1 -0
  5. package/lib/esm/index.js +480 -0
  6. package/lib/esm/index.js.map +1 -0
  7. package/lib/types/index.d.ts +12 -0
  8. package/lib/types/index.d.ts.map +1 -0
  9. package/package.json +50 -36
  10. package/sanity.json +7 -6
  11. package/src/components/Feedback.tsx +27 -0
  12. package/src/components/InternationalizedArrayInput.tsx +222 -0
  13. package/src/{LanguageArray → components}/Table.tsx +0 -0
  14. package/src/components/createFieldName.ts +20 -0
  15. package/src/components/getToneFromValidation.ts +18 -0
  16. package/src/index.ts +1 -3
  17. package/src/plugin.tsx +23 -0
  18. package/src/schema/array.ts +69 -0
  19. package/src/schema/object.ts +29 -0
  20. package/src/types.ts +19 -9
  21. package/v2-incompatible.js +11 -0
  22. package/lib/LanguageArray/Table.js +0 -88
  23. package/lib/LanguageArray/Table.js.map +0 -1
  24. package/lib/LanguageArray/ValueInput.js +0 -17
  25. package/lib/LanguageArray/ValueInput.js.map +0 -1
  26. package/lib/LanguageArray/index.js +0 -253
  27. package/lib/LanguageArray/index.js.map +0 -1
  28. package/lib/hooks/useUnsetInputComponent.js +0 -32
  29. package/lib/hooks/useUnsetInputComponent.js.map +0 -1
  30. package/lib/index.js +0 -13
  31. package/lib/index.js.map +0 -1
  32. package/lib/internationalizedArray.js +0 -105
  33. package/lib/internationalizedArray.js.map +0 -1
  34. package/lib/types.js +0 -2
  35. package/lib/types.js.map +0 -1
  36. package/migrations/transformObjectToArray.js +0 -94
  37. package/src/LanguageArray/ValueInput.tsx +0 -6
  38. package/src/LanguageArray/index.tsx +0 -311
  39. package/src/hooks/useUnsetInputComponent.tsx +0 -17
  40. package/src/internationalizedArray.ts +0 -84
package/package.json CHANGED
@@ -1,57 +1,71 @@
1
1
  {
2
2
  "name": "sanity-plugin-internationalized-array",
3
- "version": "0.0.7",
4
- "description": "Store localised fields in an array to save on attributes",
5
- "main": "lib/index.js",
3
+ "version": "1.1.0",
4
+ "description": "Store localized fields in an array to save on attributes",
5
+ "author": "Simeon Griggs <simeon@sanity.io>",
6
+ "license": "MIT",
7
+ "source": "./src/index.ts",
8
+ "main": "./lib/cjs/index.js",
9
+ "module": "./lib/esm/index.js",
10
+ "types": "./lib/types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "require": "./lib/cjs/index.js",
14
+ "default": "./lib/esm/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "src",
19
+ "lib",
20
+ "v2-incompatible.js",
21
+ "sanity.json"
22
+ ],
6
23
  "scripts": {
7
- "build": "sanipack build",
8
- "verify": "sanipack verify",
9
- "watch": "sanipack build --watch",
10
- "_postinstall": "husky install",
11
- "prepublishOnly": "pinst --disable && sanipack build && sanipack verify",
12
- "postpublish": "pinst --enable",
24
+ "clean": "rimraf lib",
13
25
  "lint": "eslint .",
14
- "lint:fix": "eslint . --fix"
15
- },
16
- "husky": {
17
- "hooks": {
18
- "pre-commit": "npm run lint:fix"
19
- }
26
+ "prebuild": "npm run clean && plugin-kit verify-package --silent",
27
+ "build": "parcel build --no-cache",
28
+ "watch": "parcel watch",
29
+ "link-watch": "plugin-kit link-watch",
30
+ "prepublishOnly": "npm run build"
20
31
  },
21
32
  "repository": {
22
33
  "type": "git",
23
34
  "url": "git+ssh://git@github.com/SimeonGriggs/sanity-plugin-internationalized-array.git"
24
35
  },
25
- "keywords": [
26
- "sanity",
27
- "sanity-plugin"
28
- ],
29
- "author": "Sanity.io <hello@sanity.io>",
30
- "license": "MIT",
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ },
31
39
  "dependencies": {
32
40
  "@sanity/icons": "^1.3.1",
33
- "@sanity/ui": "^0.37.9",
41
+ "@sanity/incompatible-plugin": "^0.0.1-studio-v3.1",
42
+ "@sanity/ui": "^0.37.12",
43
+ "sanity-plugin-utils": "^0.0.1",
34
44
  "styled-components": "^5.3.5"
35
45
  },
36
- "peerDependencies": {
37
- "@sanity/base": "^2.30.1",
38
- "@sanity/desk-tool": "^2.30.1",
39
- "@sanity/form-builder": "^2.30.1",
40
- "@sanity/util": "^2.29.5",
41
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0"
42
- },
43
46
  "devDependencies": {
44
- "@sanity/eslint-config-studio": "^2.0.0",
45
- "eslint": "8.19.0",
47
+ "@parcel/packager-ts": "^2.6.2",
48
+ "@parcel/transformer-typescript-types": "^2.6.2",
49
+ "@sanity/plugin-kit": "^0.1.0-v3-studio.1",
50
+ "@types/styled-components": "^5.1.25",
51
+ "@typescript-eslint/eslint-plugin": "^5.30.7",
52
+ "@typescript-eslint/parser": "^5.30.7",
53
+ "eslint": "^8.20.0",
46
54
  "eslint-config-prettier": "^8.5.0",
47
- "eslint-config-sanity": "6.0.0",
55
+ "eslint-config-sanity": "^6.0.0",
48
56
  "eslint-plugin-prettier": "^4.2.1",
49
57
  "eslint-plugin-react": "^7.30.1",
50
- "husky": "^8.0.1",
51
- "pinst": "^3.0.0",
58
+ "eslint-plugin-react-hooks": "^4.6.0",
59
+ "parcel": "^2.6.2",
52
60
  "prettier": "^2.7.1",
53
- "sanipack": "^2.1.0",
54
- "typescript": "^4.7.4"
61
+ "react": "^17.0.0 || ^18.0.0",
62
+ "rimraf": "^3.0.2",
63
+ "sanity": "2.29.5-purple-unicorn.856",
64
+ "typescript": "4.7.4"
65
+ },
66
+ "peerDependencies": {
67
+ "react": "^17.0.0 || ^18.0.0",
68
+ "sanity": "purple-unicorn"
55
69
  },
56
70
  "bugs": {
57
71
  "url": "https://github.com/SimeonGriggs/sanity-plugin-internationalized-array/issues"
package/sanity.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
- "paths": {
3
- "source": "./src",
4
- "compiled": "./lib"
5
- },
6
- "parts": []
7
- }
2
+ "parts": [
3
+ {
4
+ "implements": "part:@sanity/base/sanity-root",
5
+ "path": "./v2-incompatible.js"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,27 @@
1
+ import {Text, Card, Stack, Code} from '@sanity/ui'
2
+ import React from 'react'
3
+
4
+ const schemaExample = {
5
+ languages: [
6
+ {id: 'en', title: 'English'},
7
+ {id: 'no', title: 'Norsk'},
8
+ ],
9
+ }
10
+
11
+ export default function Feedback() {
12
+ return (
13
+ <Card tone="caution" border radius={2} padding={3}>
14
+ <Stack space={4}>
15
+ <Text>
16
+ An array of language objects must be passed into the <code>internationalizedArray</code>{' '}
17
+ helper function, each with an <code>id</code> and <code>title</code> field. Example:
18
+ </Text>
19
+ <Card padding={2} border radius={2}>
20
+ <Code size={1} language="javascript">
21
+ {JSON.stringify(schemaExample, null, 2)}
22
+ </Code>
23
+ </Card>
24
+ </Stack>
25
+ </Card>
26
+ )
27
+ }
@@ -0,0 +1,222 @@
1
+ import React, {useCallback, useMemo} from 'react'
2
+ import {
3
+ ArrayOfObjectsInputProps,
4
+ MemberItem,
5
+ unset,
6
+ insert,
7
+ set,
8
+ setIfMissing,
9
+ FormFieldValidationStatus,
10
+ } from 'sanity/form'
11
+ import {Box, Button, Flex, Grid, Label, Stack} from '@sanity/ui'
12
+
13
+ import {Language, Value, ArraySchemaWithLanguageOptions} from '../types'
14
+ import {Table, TableCell, TableRow} from './Table'
15
+ import {AddIcon, RemoveIcon, RestoreIcon} from '@sanity/icons'
16
+ import Feedback from './Feedback'
17
+ import {getToneFromValidation} from './getToneFromValidation'
18
+
19
+ export type InternationalizedArrayInputProps = ArrayOfObjectsInputProps<
20
+ Value,
21
+ ArraySchemaWithLanguageOptions
22
+ >
23
+
24
+ export default function InternationalizedArrayInput(props: InternationalizedArrayInputProps) {
25
+ const {members, value, schemaType, onChange} = props
26
+ const readOnly = typeof schemaType.readOnly === 'boolean' ? schemaType.readOnly : false
27
+ const {options} = schemaType
28
+
29
+ const languages: Language[] = useMemo(() => options?.languages ?? [], [options])
30
+
31
+ const handleAddLanguage = useCallback(
32
+ (languageId?: string) => {
33
+ // Create new items
34
+ const newItems = languageId
35
+ ? // Just one for this language
36
+ [{_key: languageId}]
37
+ : // Or one for every missing language
38
+ languages
39
+ .filter((language) =>
40
+ value?.length ? !value.find((v) => v._key === language.id) : true
41
+ )
42
+ .map((language) => ({_key: language.id}))
43
+
44
+ // Insert new items in the correct order
45
+ const languagesInUse = value?.length ? value.map((v) => v) : []
46
+
47
+ const insertions = newItems.map((item) => {
48
+ // What's the original index of this language?
49
+ const languageIndex = languages.findIndex((l) => item._key === l.id)
50
+
51
+ // What languages are there beyond that index?
52
+ const remainingLanguages = languages.slice(languageIndex + 1)
53
+
54
+ // So what is the index in the current value array of the next language in the language array?
55
+ const nextLanguageIndex = languagesInUse.findIndex((l) =>
56
+ // eslint-disable-next-line max-nested-callbacks
57
+ remainingLanguages.find((r) => r.id === l._key)
58
+ )
59
+
60
+ // Keep local state up to date incase multiple insertions are being made
61
+ if (nextLanguageIndex < 0) {
62
+ languagesInUse.push(item)
63
+ } else {
64
+ languagesInUse.splice(nextLanguageIndex, 0, item)
65
+ }
66
+
67
+ return nextLanguageIndex < 0
68
+ ? // No next language (-1), add to end of array
69
+ insert([item], 'after', [nextLanguageIndex])
70
+ : // Next language found, insert before that
71
+ insert([item], 'before', [nextLanguageIndex])
72
+ })
73
+
74
+ onChange([setIfMissing([]), ...insertions])
75
+ },
76
+ [languages, onChange, value]
77
+ )
78
+
79
+ const handleUnsetByKey = useCallback(
80
+ (_key: string) => {
81
+ onChange(unset([{_key}]))
82
+ },
83
+ [onChange]
84
+ )
85
+
86
+ // TODO: This is lazy, reordering and re-setting the whole array – it could be surgical
87
+ const handleRestoreOrder = useCallback(() => {
88
+ if (!value?.length) {
89
+ return
90
+ }
91
+
92
+ // Create a new value array in the correct order
93
+ // This would also strip out values that don't have a language as the key
94
+ const updatedValue = value
95
+ .reduce((acc, v) => {
96
+ const newIndex = languages.findIndex((l) => l.id === v?._key)
97
+
98
+ if (newIndex) {
99
+ acc[newIndex] = v
100
+ }
101
+
102
+ return acc
103
+ }, [] as Value[])
104
+ .filter(Boolean)
105
+
106
+ onChange(set(updatedValue))
107
+ }, [languages, onChange, value])
108
+
109
+ const allKeysAreLanguages = useMemo(() => {
110
+ return value?.every((v) => languages.find((l) => l?.id === v?._key))
111
+ }, [value, languages])
112
+
113
+ // Check languages are in the correct order
114
+ const languagesOutOfOrder = useMemo(() => {
115
+ if (!value?.length) {
116
+ return []
117
+ }
118
+
119
+ const languagesInUse = languages.filter((l) => value.find((v) => v._key === l.id))
120
+
121
+ return value
122
+ .map((v, vIndex) => (vIndex === languagesInUse.findIndex((l) => l.id === v._key) ? null : v))
123
+ .filter(Boolean)
124
+ }, [value, languages])
125
+
126
+ const languagesAreValid = useMemo(
127
+ () =>
128
+ !languages?.length || (languages?.length && languages.every((item) => item.id && item.title)),
129
+ [languages]
130
+ )
131
+
132
+ if (!languagesAreValid) {
133
+ return <Feedback />
134
+ }
135
+
136
+ return (
137
+ <Stack space={2}>
138
+ {members?.length > 0 ? (
139
+ <Table>
140
+ <tbody>
141
+ {members.map((member) => (
142
+ <TableRow
143
+ key={member.key}
144
+ tone={
145
+ member?.item?.validation?.length > 0
146
+ ? getToneFromValidation(member.item.validation)
147
+ : undefined
148
+ }
149
+ >
150
+ <TableCell style={{verticalAlign: 'bottom'}}>
151
+ <Box paddingY={3} paddingRight={2}>
152
+ <Label muted size={1}>
153
+ {member.key}
154
+ </Label>
155
+ </Box>
156
+ </TableCell>
157
+ <TableCell paddingRight={2} style={{width: `100%`}}>
158
+ {/* This renders the entire field default with title */}
159
+ <MemberItem {...props} member={member} />
160
+ </TableCell>
161
+ <TableCell style={{verticalAlign: 'bottom'}}>
162
+ <Flex align="center" justify="flex-end" gap={3}>
163
+ {/* Possibly unncessary, validation shows up in <MemberItem /> */}
164
+ {member?.item?.validation?.length > 0 ? (
165
+ <Box paddingLeft={2}>
166
+ <FormFieldValidationStatus validation={member.item.validation} />
167
+ </Box>
168
+ ) : null}
169
+ <Button
170
+ mode="ghost"
171
+ icon={RemoveIcon}
172
+ tone="critical"
173
+ disabled={typeof readOnly === 'boolean' ? readOnly : false}
174
+ onClick={() => handleUnsetByKey(member.key)}
175
+ />
176
+ </Flex>
177
+ </TableCell>
178
+ </TableRow>
179
+ ))}
180
+ </tbody>
181
+ </Table>
182
+ ) : null}
183
+
184
+ {languagesOutOfOrder.length > 0 && allKeysAreLanguages ? (
185
+ <Button
186
+ tone="caution"
187
+ icon={RestoreIcon}
188
+ onClick={() => handleRestoreOrder()}
189
+ text="Restore order of languages"
190
+ />
191
+ ) : null}
192
+
193
+ {languages?.length > 0 ? (
194
+ <Stack space={2}>
195
+ {/* No more than 5 columns */}
196
+ <Grid columns={Math.min(languages.length, 5)} gap={2}>
197
+ {languages.map((language) => (
198
+ <Button
199
+ key={language.id}
200
+ tone="primary"
201
+ mode="ghost"
202
+ fontSize={1}
203
+ disabled={readOnly || Boolean(value?.find((item) => item._key === language.id))}
204
+ text={language.id.toUpperCase()}
205
+ icon={AddIcon}
206
+ onClick={() => handleAddLanguage(language.id)}
207
+ />
208
+ ))}
209
+ </Grid>
210
+ <Button
211
+ tone="primary"
212
+ mode="ghost"
213
+ disabled={readOnly || (value && value?.length >= languages?.length)}
214
+ icon={AddIcon}
215
+ text={value?.length ? `Add missing languages` : `Add all languages`}
216
+ onClick={() => handleAddLanguage()}
217
+ />
218
+ </Stack>
219
+ ) : null}
220
+ </Stack>
221
+ )
222
+ }
File without changes
@@ -0,0 +1,20 @@
1
+ export function camelCase(string: string) {
2
+ return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
3
+ }
4
+
5
+ export function titleCase(string: string) {
6
+ return string
7
+ .split(` `)
8
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
9
+ .join(` `)
10
+ }
11
+
12
+ export function pascalCase(string: string) {
13
+ return titleCase(camelCase(string))
14
+ }
15
+
16
+ export function createFieldName(name: string, addValue = false): string {
17
+ return addValue
18
+ ? [`internationalizedArray`, pascalCase(name), `Value`].join(``)
19
+ : [`internationalizedArray`, pascalCase(name)].join(``)
20
+ }
@@ -0,0 +1,18 @@
1
+ import {NodeValidation} from 'sanity/form'
2
+ import {CardTone} from '@sanity/ui'
3
+
4
+ export function getToneFromValidation(validations: NodeValidation[]): CardTone {
5
+ if (!validations.length) {
6
+ return `default`
7
+ }
8
+
9
+ const validationLevels = validations.map((v) => v.level)
10
+
11
+ if (validationLevels.includes('error')) {
12
+ return `critical`
13
+ } else if (validationLevels.includes('warning')) {
14
+ return `caution`
15
+ }
16
+
17
+ return `default`
18
+ }
package/src/index.ts CHANGED
@@ -1,3 +1 @@
1
- import {internationalizedArray as helperFunction} from './internationalizedArray'
2
-
3
- export const internationalizedArray = (config) => helperFunction(config)
1
+ export {internationalizedArray} from './plugin'
package/src/plugin.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import {createPlugin} from 'sanity'
2
+ import {PluginConfig} from './types'
3
+ import array from './schema/array'
4
+ import object from './schema/object'
5
+
6
+ const CONFIG_DEFAULT = {
7
+ languages: [],
8
+ fieldTypes: [],
9
+ }
10
+
11
+ export const internationalizedArray = createPlugin<PluginConfig>((config = CONFIG_DEFAULT) => {
12
+ const {languages, fieldTypes} = {...CONFIG_DEFAULT, ...config}
13
+
14
+ return {
15
+ name: 'sanity-plugin-internationalized-array',
16
+ schema: {
17
+ types: [
18
+ ...fieldTypes.map((type) => array({type, languages})),
19
+ ...fieldTypes.map((type) => object({type})),
20
+ ],
21
+ },
22
+ }
23
+ })
@@ -0,0 +1,69 @@
1
+ import {defineField, Rule, Schema} from 'sanity'
2
+
3
+ import {createFieldName} from '../components/createFieldName'
4
+ import InternationalizedArrayInput from '../components/InternationalizedArrayInput'
5
+ import {Language, Value} from '../types'
6
+
7
+ type ArrayFactoryConfig = {
8
+ languages: Language[]
9
+ type: string | Schema.FieldDefinition
10
+ }
11
+
12
+ export default (config: ArrayFactoryConfig): Schema.FieldDefinition<'array'> => {
13
+ const {languages, type} = config
14
+ const typeName = typeof type === `string` ? type : type.name
15
+ const arrayName = createFieldName(typeName)
16
+ const objectName = createFieldName(typeName, true)
17
+
18
+ return defineField({
19
+ name: arrayName,
20
+ title: 'Internationalized array',
21
+ type: 'array',
22
+ components: {input: InternationalizedArrayInput},
23
+ options: {languages},
24
+ of: [defineField({name: objectName, type: objectName})],
25
+ validation: (rule: Rule) =>
26
+ rule.max(languages?.length).custom<Value[]>((value, context) => {
27
+ const {languages: contextLanguages}: {languages: Language[]} = context?.type?.options ?? {}
28
+ const nonLanguageKeys = value?.length
29
+ ? value.filter((item) => !contextLanguages.find((language) => item._key === language.id))
30
+ : []
31
+ if (nonLanguageKeys.length) {
32
+ return {
33
+ message: `Array item keys must be valid languages registered to the field type`,
34
+ paths: nonLanguageKeys.map((item) => [{_key: item._key}]),
35
+ }
36
+ }
37
+
38
+ // Ensure there's no duplicate `language` fields
39
+ type KeyedValues = {
40
+ [key: string]: Value[]
41
+ }
42
+
43
+ const valuesByLanguage = value?.length
44
+ ? value
45
+ .filter((item) => Boolean(item?._key))
46
+ .reduce((acc, cur) => {
47
+ if (acc[cur._key]) {
48
+ return {...acc, [cur._key]: [...acc[cur._key], cur]}
49
+ }
50
+ return {
51
+ ...acc,
52
+ [cur._key]: [cur],
53
+ }
54
+ }, {} as KeyedValues)
55
+ : {}
56
+ const duplicateValues = Object.values(valuesByLanguage)
57
+ .filter((item) => item?.length > 1)
58
+ .flat()
59
+ if (duplicateValues.length) {
60
+ return {
61
+ message: 'There can only be one field per language',
62
+ paths: duplicateValues.map((item) => [{_key: item._key}]),
63
+ }
64
+ }
65
+
66
+ return true
67
+ }),
68
+ })
69
+ }
@@ -0,0 +1,29 @@
1
+ import {defineField, Schema} from 'sanity'
2
+
3
+ import {createFieldName} from '../components/createFieldName'
4
+
5
+ type ObjectFactoryConfig = {
6
+ type: string | Schema.FieldDefinition
7
+ }
8
+
9
+ export default (config: ObjectFactoryConfig): Schema.FieldDefinition<'object'> => {
10
+ const {type} = config
11
+ const typeName = typeof type === `string` ? type : type.name
12
+ const objectName = createFieldName(typeName, true)
13
+
14
+ return defineField({
15
+ name: objectName,
16
+ title: `Internationalized array ${type}`,
17
+ type: 'object',
18
+ fields: [
19
+ typeof type === `string`
20
+ ? // Define a basic field if all we have is the string name
21
+ defineField({
22
+ name: 'value',
23
+ type,
24
+ })
25
+ : // Pass in the configured options, but overwrite the name
26
+ {...type, name: 'value'},
27
+ ],
28
+ })
29
+ }
package/src/types.ts CHANGED
@@ -1,13 +1,22 @@
1
- import {Rule} from '@sanity/types'
1
+ import {Rule, ArraySchemaType, Schema} from 'sanity'
2
2
 
3
- export type ArrayConfig = Options & {
3
+ export type Language = {
4
+ id: string
5
+ title: string
6
+ }
7
+
8
+ export type AllowedType = 'string' | 'number' | 'boolean' | 'text' | 'reference'
9
+
10
+ export type ArrayConfig = {
4
11
  name: string
5
- type: 'string' | 'number' | 'boolean' | 'text'
12
+ type: AllowedType
13
+ languages: Language[]
6
14
  title?: string
7
15
  group?: string
8
16
  hidden?: boolean | (() => boolean)
9
17
  readOnly?: boolean | (() => boolean)
10
18
  validation?: Rule | Rule[]
19
+ field?: {[key: string]: any; options: {[key: string]: any}}
11
20
  }
12
21
 
13
22
  export type Value = {
@@ -15,12 +24,13 @@ export type Value = {
15
24
  value?: string
16
25
  }
17
26
 
18
- export type Language = {
19
- id: string
20
- title: string
27
+ export type PluginConfig = {
28
+ languages: Language[]
29
+ fieldTypes: (string | Schema.FieldDefinition)[]
21
30
  }
22
31
 
23
- export type Options = {
24
- languages: Language[]
25
- showNativeInput: boolean
32
+ export type ArraySchemaWithLanguageOptions = ArraySchemaType & {
33
+ options: {
34
+ languages: Language[]
35
+ }
26
36
  }
@@ -0,0 +1,11 @@
1
+ const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
+ const {name, version, sanityExchangeUrl} = require('./package.json')
3
+
4
+ export default showIncompatiblePluginDialog({
5
+ name: name,
6
+ versions: {
7
+ v3: version,
8
+ v2: undefined,
9
+ },
10
+ sanityExchangeUrl,
11
+ })