@sanity/cli 7.18.0 → 8.0.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 (131) hide show
  1. package/README.md +74 -4
  2. package/dist/SanityHelp.js +1 -35
  3. package/dist/SanityHelp.js.map +1 -1
  4. package/dist/actions/assets/assetFileError.js +9 -0
  5. package/dist/actions/assets/assetFileError.js.map +1 -0
  6. package/dist/actions/assets/uploadAssetFromFile.js +40 -0
  7. package/dist/actions/assets/uploadAssetFromFile.js.map +1 -0
  8. package/dist/actions/assets/uploadAssetWithProgress.js +76 -0
  9. package/dist/actions/assets/uploadAssetWithProgress.js.map +1 -0
  10. package/dist/actions/deploy/deployApp.js +39 -44
  11. package/dist/actions/deploy/deployApp.js.map +1 -1
  12. package/dist/actions/deploy/deployChecks.js +21 -13
  13. package/dist/actions/deploy/deployChecks.js.map +1 -1
  14. package/dist/actions/deploy/deployRunner.js +8 -7
  15. package/dist/actions/deploy/deployRunner.js.map +1 -1
  16. package/dist/actions/deploy/deployStudio.js +55 -35
  17. package/dist/actions/deploy/deployStudio.js.map +1 -1
  18. package/dist/actions/deploy/deploymentPlan.js +17 -29
  19. package/dist/actions/deploy/deploymentPlan.js.map +1 -1
  20. package/dist/actions/deploy/resolveDeployTarget.js +2 -8
  21. package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
  22. package/dist/actions/dev/servers/getDevServerConfig.js +2 -8
  23. package/dist/actions/dev/servers/getDevServerConfig.js.map +1 -1
  24. package/dist/actions/dev/servers/startStudioDevServer.js +7 -1
  25. package/dist/actions/dev/servers/startStudioDevServer.js.map +1 -1
  26. package/dist/actions/init/bootstrapRemoteTemplate.js +2 -2
  27. package/dist/actions/init/bootstrapRemoteTemplate.js.map +1 -1
  28. package/dist/actions/schema/listSchemas.js +1 -1
  29. package/dist/actions/schema/listSchemas.js.map +1 -1
  30. package/dist/actions/tokens/constants.js +2 -2
  31. package/dist/actions/tokens/constants.js.map +1 -1
  32. package/dist/actions/tokens/expiry.js +35 -0
  33. package/dist/actions/tokens/expiry.js.map +1 -0
  34. package/dist/actions/tokens/types.js.map +1 -1
  35. package/dist/actions/tokens/validateRole.js +5 -2
  36. package/dist/actions/tokens/validateRole.js.map +1 -1
  37. package/dist/actions/typegen/count.js +6 -0
  38. package/dist/actions/typegen/count.js.map +1 -0
  39. package/dist/actions/typegen/formatPath.js +5 -0
  40. package/dist/actions/typegen/formatPath.js.map +1 -0
  41. package/dist/actions/typegen/percent.js +8 -0
  42. package/dist/actions/typegen/percent.js.map +1 -0
  43. package/dist/actions/typegen/renderTypegenProgress.js +88 -0
  44. package/dist/actions/typegen/renderTypegenProgress.js.map +1 -0
  45. package/dist/actions/undeploy/adapters.js +6 -9
  46. package/dist/actions/undeploy/adapters.js.map +1 -1
  47. package/dist/actions/undeploy/runUndeploy.js +12 -4
  48. package/dist/actions/undeploy/runUndeploy.js.map +1 -1
  49. package/dist/actions/undeploy/undeployPlan.js +6 -14
  50. package/dist/actions/undeploy/undeployPlan.js.map +1 -1
  51. package/dist/commands/assets/upload.js +126 -0
  52. package/dist/commands/assets/upload.js.map +1 -0
  53. package/dist/commands/backups/list.js +1 -1
  54. package/dist/commands/backups/list.js.map +1 -1
  55. package/dist/commands/datasets/copy.js +1 -1
  56. package/dist/commands/datasets/copy.js.map +1 -1
  57. package/dist/commands/login.js +2 -8
  58. package/dist/commands/login.js.map +1 -1
  59. package/dist/commands/mcp/configure.js.map +1 -1
  60. package/dist/commands/migrations/list.js +1 -1
  61. package/dist/commands/migrations/list.js.map +1 -1
  62. package/dist/commands/migrations/run.js +67 -3
  63. package/dist/commands/migrations/run.js.map +1 -1
  64. package/dist/commands/new.js +4 -3
  65. package/dist/commands/new.js.map +1 -1
  66. package/dist/commands/organizations/list.js +1 -1
  67. package/dist/commands/organizations/list.js.map +1 -1
  68. package/dist/commands/projects/list.js +39 -12
  69. package/dist/commands/projects/list.js.map +1 -1
  70. package/dist/commands/projects/unclaimed.js +39 -33
  71. package/dist/commands/projects/unclaimed.js.map +1 -1
  72. package/dist/commands/tokens/create.js +92 -7
  73. package/dist/commands/tokens/create.js.map +1 -1
  74. package/dist/commands/tokens/delete.js +2 -2
  75. package/dist/commands/tokens/delete.js.map +1 -1
  76. package/dist/commands/tokens/list.js +12 -11
  77. package/dist/commands/tokens/list.js.map +1 -1
  78. package/dist/commands/tokens/rotate.js +95 -0
  79. package/dist/commands/tokens/rotate.js.map +1 -0
  80. package/dist/commands/typegen/generate.js +222 -0
  81. package/dist/commands/typegen/generate.js.map +1 -0
  82. package/dist/commands/users/list.js +8 -8
  83. package/dist/commands/users/list.js.map +1 -1
  84. package/dist/exports/_internal.d.ts +29 -0
  85. package/dist/exports/_internal.js +1 -0
  86. package/dist/exports/_internal.js.map +1 -1
  87. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +6 -0
  88. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  89. package/dist/exports/invokeSanityCli/help.js +4 -0
  90. package/dist/exports/invokeSanityCli/help.js.map +1 -1
  91. package/dist/exports/invokeSanityCli/index.js +2 -0
  92. package/dist/exports/invokeSanityCli/index.js.map +1 -1
  93. package/dist/hooks/commandNotFound/topicAliases.js +13 -40
  94. package/dist/hooks/commandNotFound/topicAliases.js.map +1 -1
  95. package/dist/hooks/prerun/setupTelemetry.js +3 -2
  96. package/dist/hooks/prerun/setupTelemetry.js.map +1 -1
  97. package/dist/server/vite/plugin-typegen.js +4 -16
  98. package/dist/server/vite/plugin-typegen.js.map +1 -1
  99. package/dist/services/assets.js +46 -0
  100. package/dist/services/assets.js.map +1 -0
  101. package/dist/services/tokens.js +81 -31
  102. package/dist/services/tokens.js.map +1 -1
  103. package/dist/topicAliases.js +43 -0
  104. package/dist/topicAliases.js.map +1 -1
  105. package/dist/util/parseArguments.js +7 -2
  106. package/dist/util/parseArguments.js.map +1 -1
  107. package/dist/util/readTokenFromStdin.js +17 -0
  108. package/dist/util/readTokenFromStdin.js.map +1 -0
  109. package/dist/util/responsiveTable.js +97 -0
  110. package/dist/util/responsiveTable.js.map +1 -0
  111. package/dist/util/telemetry/commandTelemetry.js +34 -0
  112. package/dist/util/telemetry/commandTelemetry.js.map +1 -0
  113. package/dist/util/unclaimedProjects.js +10 -0
  114. package/dist/util/unclaimedProjects.js.map +1 -1
  115. package/oclif.config.js +2 -4
  116. package/oclif.manifest.json +213 -1
  117. package/package.json +12 -10
  118. package/templates/shopify/components/inputs/ShopifyMetafield.tsx +20 -0
  119. package/templates/shopify/docs/features.md +24 -0
  120. package/templates/shopify/schemaTypes/index.ts +2 -0
  121. package/templates/shopify/schemaTypes/objects/shopify/shopifyCollectionType.ts +18 -1
  122. package/templates/shopify/schemaTypes/objects/shopify/shopifyMetafieldType.ts +47 -0
  123. package/templates/shopify/schemaTypes/objects/shopify/shopifyProductType.ts +17 -0
  124. package/templates/shopify/utils/formatMetafieldValue.ts +30 -0
  125. package/templates/shopify-online-storefront/components/inputs/ShopifyMetafield.tsx +22 -0
  126. package/templates/shopify-online-storefront/schemaTypes/index.ts +4 -0
  127. package/templates/shopify-online-storefront/schemaTypes/objects/shop.ts +16 -0
  128. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyCollection.ts +19 -0
  129. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyMetafield.ts +53 -0
  130. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyProduct.ts +19 -0
  131. package/templates/shopify-online-storefront/utils/formatMetafieldValue.ts +30 -0
@@ -1344,6 +1344,112 @@
1344
1344
  "versions.js"
1345
1345
  ]
1346
1346
  },
1347
+ "assets:upload": {
1348
+ "aliases": [],
1349
+ "args": {},
1350
+ "description": "Upload one local image or file to a Sanity dataset and print the asset document as JSON",
1351
+ "examples": [
1352
+ {
1353
+ "command": "<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production",
1354
+ "description": "Upload an image using the configured project"
1355
+ },
1356
+ {
1357
+ "command": "<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production",
1358
+ "description": "Upload a file with explicit project, dataset, and MIME type"
1359
+ }
1360
+ ],
1361
+ "flags": {
1362
+ "project-id": {
1363
+ "char": "p",
1364
+ "description": "Project ID to upload the asset to (overrides CLI configuration)",
1365
+ "helpGroup": "OVERRIDE",
1366
+ "name": "project-id",
1367
+ "hasDynamicHelp": false,
1368
+ "helpValue": "<id>",
1369
+ "multiple": false,
1370
+ "type": "option"
1371
+ },
1372
+ "dataset": {
1373
+ "char": "d",
1374
+ "description": "Dataset to upload the asset to (overrides CLI configuration)",
1375
+ "helpGroup": "OVERRIDE",
1376
+ "name": "dataset",
1377
+ "hasDynamicHelp": false,
1378
+ "helpValue": "<name>",
1379
+ "multiple": false,
1380
+ "type": "option"
1381
+ },
1382
+ "content-type": {
1383
+ "description": "MIME type of the asset, such as image/png or application/pdf",
1384
+ "name": "content-type",
1385
+ "hasDynamicHelp": false,
1386
+ "helpValue": "<mime-type>",
1387
+ "multiple": false,
1388
+ "type": "option"
1389
+ },
1390
+ "file": {
1391
+ "description": "Path to the local file to upload",
1392
+ "name": "file",
1393
+ "required": true,
1394
+ "hasDynamicHelp": false,
1395
+ "helpValue": "<path>",
1396
+ "multiple": false,
1397
+ "type": "option"
1398
+ },
1399
+ "filename": {
1400
+ "description": "Original filename stored on the asset document. Defaults to the local filename",
1401
+ "name": "filename",
1402
+ "hasDynamicHelp": false,
1403
+ "helpValue": "<filename>",
1404
+ "multiple": false,
1405
+ "type": "option"
1406
+ },
1407
+ "type": {
1408
+ "description": "Asset type to create",
1409
+ "name": "type",
1410
+ "default": "image",
1411
+ "hasDynamicHelp": false,
1412
+ "multiple": false,
1413
+ "options": [
1414
+ "image",
1415
+ "file"
1416
+ ],
1417
+ "type": "option"
1418
+ }
1419
+ },
1420
+ "hasDynamicHelp": false,
1421
+ "hiddenAliases": [
1422
+ "asset:upload"
1423
+ ],
1424
+ "id": "assets:upload",
1425
+ "pluginAlias": "@sanity/cli",
1426
+ "pluginName": "@sanity/cli",
1427
+ "pluginType": "core",
1428
+ "strict": true,
1429
+ "telemetry": {
1430
+ "redactedFlags": [
1431
+ {
1432
+ "canonical": "--file",
1433
+ "flags": [
1434
+ "--file"
1435
+ ]
1436
+ },
1437
+ {
1438
+ "canonical": "--filename",
1439
+ "flags": [
1440
+ "--filename"
1441
+ ]
1442
+ }
1443
+ ]
1444
+ },
1445
+ "isESM": true,
1446
+ "relativePath": [
1447
+ "dist",
1448
+ "commands",
1449
+ "assets",
1450
+ "upload.js"
1451
+ ]
1452
+ },
1347
1453
  "backups:disable": {
1348
1454
  "aliases": [],
1349
1455
  "args": {
@@ -5097,6 +5203,10 @@
5097
5203
  "command": "<%= config.bin %> <%= command.id %> \"CI Token\" --role=editor --yes",
5098
5204
  "description": "Create a token in unattended mode"
5099
5205
  },
5206
+ {
5207
+ "command": "<%= config.bin %> <%= command.id %> \"Build Token\" --expires-at 2027-01-01",
5208
+ "description": "Create a token that expires on a given date"
5209
+ },
5100
5210
  {
5101
5211
  "command": "<%= config.bin %> <%= command.id %> \"API Token\" --json",
5102
5212
  "description": "Output token information as JSON"
@@ -5117,6 +5227,14 @@
5117
5227
  "multiple": false,
5118
5228
  "type": "option"
5119
5229
  },
5230
+ "expires-at": {
5231
+ "description": "Date or timestamp the token expires (ISO 8601; tokens never expire by default)",
5232
+ "name": "expires-at",
5233
+ "hasDynamicHelp": false,
5234
+ "helpValue": "2027-01-01",
5235
+ "multiple": false,
5236
+ "type": "option"
5237
+ },
5120
5238
  "json": {
5121
5239
  "description": "Output as JSON",
5122
5240
  "name": "json",
@@ -5278,6 +5396,100 @@
5278
5396
  "list.js"
5279
5397
  ]
5280
5398
  },
5399
+ "tokens:rotate": {
5400
+ "aliases": [],
5401
+ "args": {},
5402
+ "description": "Rotate an API token, replacing its secret with a new one",
5403
+ "examples": [
5404
+ {
5405
+ "command": "echo \"$SANITY_TOKEN\" | <%= config.bin %> <%= command.id %>",
5406
+ "description": "Rotate the token piped on standard input"
5407
+ },
5408
+ {
5409
+ "command": "<%= config.bin %> <%= command.id %> < token.txt",
5410
+ "description": "Rotate a token read from a file"
5411
+ },
5412
+ {
5413
+ "command": "echo \"$SANITY_TOKEN\" | <%= config.bin %> <%= command.id %> --json",
5414
+ "description": "Output the rotated token as JSON"
5415
+ }
5416
+ ],
5417
+ "flags": {
5418
+ "json": {
5419
+ "description": "Output as JSON",
5420
+ "name": "json",
5421
+ "allowNo": false,
5422
+ "type": "boolean"
5423
+ },
5424
+ "token": {
5425
+ "char": "t",
5426
+ "description": "Token to rotate (prefer standard input to keep it out of shell history)",
5427
+ "name": "token",
5428
+ "hasDynamicHelp": false,
5429
+ "helpValue": "<token>",
5430
+ "multiple": false,
5431
+ "type": "option"
5432
+ }
5433
+ },
5434
+ "hasDynamicHelp": false,
5435
+ "hiddenAliases": [
5436
+ "token:rotate"
5437
+ ],
5438
+ "id": "tokens:rotate",
5439
+ "pluginAlias": "@sanity/cli",
5440
+ "pluginName": "@sanity/cli",
5441
+ "pluginType": "core",
5442
+ "strict": true,
5443
+ "isESM": true,
5444
+ "relativePath": [
5445
+ "dist",
5446
+ "commands",
5447
+ "tokens",
5448
+ "rotate.js"
5449
+ ]
5450
+ },
5451
+ "typegen:generate": {
5452
+ "aliases": [],
5453
+ "args": {},
5454
+ "description": "Sanity TypeGen\n\nConfiguration:\nThis command can utilize configuration settings defined in a `sanity-typegen.json` file. These settings include:\n\n- \"path\": Specifies a glob pattern to locate your TypeScript or JavaScript files.\n Default: \"./src/**/*.{ts,tsx,js,jsx}\"\n\n- \"schema\": Defines the path to your Sanity schema file. This file should be generated using the `sanity schema extract` command.\n Default: \"schema.json\"\n\n- \"generates\": Indicates the path where the generated TypeScript type definitions will be saved.\n Default: \"./sanity.types.ts\"\n\nThe default configuration values listed above are used if not overridden in your `sanity-typegen.json` configuration file. To customize the behavior of the type generation, adjust these properties in the configuration file according to your project's needs.\n\nNote:\n- The `sanity schema extract` command is a prerequisite for extracting your Sanity Studio schema into a `schema.json` file, which is then used by the `sanity typegen generate` command to generate type definitions.",
5455
+ "examples": [
5456
+ {
5457
+ "command": "<%= config.bin %> <%= command.id %>",
5458
+ "description": "Generate TypeScript type definitions from a Sanity Studio schema extracted using the `sanity schema extract` command."
5459
+ }
5460
+ ],
5461
+ "flags": {
5462
+ "config-path": {
5463
+ "description": "[Default: sanity-typegen.json] Specifies the path to the typegen configuration file. This file should be a JSON file that contains settings for the type generation process.",
5464
+ "name": "config-path",
5465
+ "hasDynamicHelp": false,
5466
+ "multiple": false,
5467
+ "type": "option"
5468
+ },
5469
+ "watch": {
5470
+ "description": "[Default: false] Run the typegen in watch mode",
5471
+ "name": "watch",
5472
+ "allowNo": false,
5473
+ "type": "boolean"
5474
+ }
5475
+ },
5476
+ "hasDynamicHelp": false,
5477
+ "hiddenAliases": [
5478
+ "typegen:generate"
5479
+ ],
5480
+ "id": "typegen:generate",
5481
+ "pluginAlias": "@sanity/cli",
5482
+ "pluginName": "@sanity/cli",
5483
+ "pluginType": "core",
5484
+ "strict": true,
5485
+ "isESM": true,
5486
+ "relativePath": [
5487
+ "dist",
5488
+ "commands",
5489
+ "typegen",
5490
+ "generate.js"
5491
+ ]
5492
+ },
5281
5493
  "users:invite": {
5282
5494
  "aliases": [],
5283
5495
  "args": {
@@ -5949,5 +6161,5 @@
5949
6161
  ]
5950
6162
  }
5951
6163
  },
5952
- "version": "7.18.0"
6164
+ "version": "8.0.0"
5953
6165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/cli",
3
- "version": "7.18.0",
3
+ "version": "8.0.0",
4
4
  "description": "Sanity CLI tool for managing Sanity projects and organizations",
5
5
  "keywords": [
6
6
  "cli",
@@ -63,14 +63,14 @@
63
63
  "@oclif/plugin-help": "^6.2.53",
64
64
  "@oclif/plugin-not-found": "^3.2.88",
65
65
  "@sanity/client": "^7.25.0",
66
- "@sanity/codegen": "^7.0.3",
66
+ "@sanity/codegen": "^8.0.0",
67
67
  "@sanity/descriptors": "^1.3.0",
68
68
  "@sanity/export": "^6.2.0",
69
69
  "@sanity/generate-help-url": "^4.0.0",
70
70
  "@sanity/id-utils": "^1.0.0",
71
71
  "@sanity/import": "^6.0.3",
72
- "@sanity/migrate": "^8.0.1",
73
- "@sanity/runtime-cli": "^17.3.0",
72
+ "@sanity/migrate": "^8.0.2",
73
+ "@sanity/runtime-cli": "^17.4.0",
74
74
  "@sanity/schema": "^6.7.0",
75
75
  "@sanity/telemetry": "^1.1.0",
76
76
  "@sanity/template-validator": "^3.1.0",
@@ -86,7 +86,7 @@
86
86
  "execa": "^9.6.0",
87
87
  "form-data": "^4.0.5",
88
88
  "get-latest-version": "^6.0.1",
89
- "groq-js": "^1.30.3",
89
+ "groq-js": "^2.0.0",
90
90
  "gunzip-maybe": "^1.4.2",
91
91
  "import-meta-resolve": "^4.2.0",
92
92
  "is-installed-globally": "^1.0.0",
@@ -113,6 +113,7 @@
113
113
  "skills": "^1.5.9",
114
114
  "smol-toml": "^1.6.1",
115
115
  "string-argv": "^0.3.2",
116
+ "string-width": "^7.2.0",
116
117
  "tar": "^7.5.18",
117
118
  "tar-fs": "^3.1.2",
118
119
  "tar-stream": "^3.2.0",
@@ -121,11 +122,12 @@
121
122
  "typeid-js": "^1.2.0",
122
123
  "vite": "^8.2.0",
123
124
  "which": "^6.0.1",
125
+ "wrap-ansi": "^9.0.2",
124
126
  "yaml": "^2.9.0",
125
127
  "zod": "^4.4.3",
126
- "@sanity/cli-build": "^5.1.0",
127
- "@sanity/cli-core": "^2.8.1",
128
- "@sanity/workbench-cli": "^1.10.0"
128
+ "@sanity/cli-build": "^5.2.0",
129
+ "@sanity/workbench-cli": "^2.0.0",
130
+ "@sanity/cli-core": "^3.0.0"
129
131
  },
130
132
  "devDependencies": {
131
133
  "@eslint/compat": "^2.1.0",
@@ -158,9 +160,9 @@
158
160
  "sanity": "^6.7.0",
159
161
  "typescript": "^6.0.3",
160
162
  "vitest": "^4.1.10",
161
- "@repo/tsconfig": "3.70.0",
162
163
  "@repo/package.config": "0.0.1",
163
- "@sanity/cli-test": "9.0.2",
164
+ "@repo/tsconfig": "3.70.0",
165
+ "@sanity/cli-test": "10.0.0",
164
166
  "@sanity/eslint-config-cli": "1.1.3"
165
167
  },
166
168
  "peerDependencies": {
@@ -0,0 +1,20 @@
1
+ import {Card, Code} from '@sanity/ui'
2
+ import {type ObjectInputProps} from 'sanity'
3
+ import {formatMetafieldValue} from '../../utils/formatMetafieldValue'
4
+
5
+ /**
6
+ * Shows a synced metafield's value. `value` isn't a declared field on `shopifyMetafield` — its shape
7
+ * depends on the Shopify metafield type — so it's read from the raw object value here.
8
+ */
9
+ export default function ShopifyMetafield(props: ObjectInputProps) {
10
+ const {type, value} = (props.value || {}) as {type?: string; value?: unknown}
11
+ const text = formatMetafieldValue(type, value)
12
+
13
+ return (
14
+ <Card border padding={3} radius={1} tone="transparent">
15
+ <Code size={1} style={{margin: 0}}>
16
+ {text}
17
+ </Code>
18
+ </Card>
19
+ )
20
+ }
@@ -12,6 +12,30 @@ Inside `/schemaTypes` you'll find schema definitions for all the content types.
12
12
  - `/schemaTypes/documents/`: Document types determines the shape of the JSON documents that's stored in your content lake. This is where you define the content forms for things like collections, products, product variants, as well as articles.
13
13
  - `/schemaTypes/objects/`: General purpose & re-usable content structures, such as links, custom product options and modules.
14
14
 
15
+ ## Shopify metafields
16
+
17
+ If you've selected metafield namespaces to import in Sanity Connect, your product and collection metafields are synced to `store.metafields` and shown under a **Metafields** fieldset.
18
+
19
+ Each entry holds the metafield's `namespace`, `key`, Shopify `type` and `value`:
20
+
21
+ ```json
22
+ {
23
+ "_key": "custom.snowboard_length",
24
+ "namespace": "custom",
25
+ "key": "snowboard_length",
26
+ "type": "dimension",
27
+ "value": {"value": 180, "unit": "CENTIMETERS"}
28
+ }
29
+ ```
30
+
31
+ `value` isn't declared on the `shopifyMetafield` schema type. Its shape follows the Shopify metafield type — a string, number, boolean, list or object — and no Sanity field type accepts all of those. `/components/inputs/ShopifyMetafield.tsx` reads it from the raw value instead.
32
+
33
+ `/utils/formatMetafieldValue.ts` decides how a value is shown, using a small map keyed by metafield type. `dimension`, `weight`, `volume`, `money`, `rating` and `list.single_line_text_field` are there as examples.
34
+
35
+ Anything not listed falls back to the raw value — a string as-is, anything else as JSON — so your own metafields still display. To show one of them nicely, add an entry to `formatters`. Shopify's [list of data types](https://shopify.dev/docs/apps/build/custom-data/metafields/list-of-data-types) gives the shape of every value.
36
+
37
+ Metafields are read-only and the whole array is replaced on every sync, so edits made here are overwritten. Variant metafields and metaobjects aren't synced.
38
+
15
39
  ## Structure
16
40
 
17
41
  Sanity Studio will automatically list all your [document types][docs-document-types] out of the box. Sometimes you want a more streamlined editor experience. That's why you'll find a custom [structure][docs-structure] that's defined in `/structure`. It does the following things:
@@ -37,6 +37,7 @@ import {productWithVariantType} from './objects/shopify/productWithVariantType'
37
37
  import {proxyStringType} from './objects/shopify/proxyStringType'
38
38
  import {seoType} from './objects/seoType'
39
39
  import {shopifyCollectionType} from './objects/shopify/shopifyCollectionType'
40
+ import {shopifyMetafieldType} from './objects/shopify/shopifyMetafieldType'
40
41
  import {shopifyProductType} from './objects/shopify/shopifyProductType'
41
42
  import {shopifyProductVariantType} from './objects/shopify/shopifyProductVariantType'
42
43
  import {shopType} from './objects/shopify/shopType'
@@ -81,6 +82,7 @@ const objects = [
81
82
  proxyStringType,
82
83
  seoType,
83
84
  shopifyCollectionType,
85
+ shopifyMetafieldType,
84
86
  shopifyProductType,
85
87
  shopifyProductVariantType,
86
88
  shopType,
@@ -1,4 +1,4 @@
1
- import {defineField} from 'sanity'
1
+ import {defineArrayMember, defineField} from 'sanity'
2
2
 
3
3
  export const shopifyCollectionType = defineField({
4
4
  name: 'shopifyCollection',
@@ -14,6 +14,14 @@ export const shopifyCollectionType = defineField({
14
14
  name: 'status',
15
15
  title: 'Status',
16
16
  },
17
+ {
18
+ name: 'metafields',
19
+ title: 'Metafields',
20
+ options: {
21
+ collapsed: true,
22
+ collapsible: true,
23
+ },
24
+ },
17
25
  ],
18
26
  fields: [
19
27
  defineField({
@@ -80,5 +88,14 @@ export const shopifyCollectionType = defineField({
80
88
  name: 'sortOrder',
81
89
  type: 'string',
82
90
  }),
91
+ defineField({
92
+ fieldset: 'metafields',
93
+ name: 'metafields',
94
+ type: 'array',
95
+ description: 'Read-only. Replaced in full on every sync',
96
+ // Shopify owns the order, and `readOnly` does not remove the drag handles on its own.
97
+ options: {sortable: false},
98
+ of: [defineArrayMember({type: 'shopifyMetafield'})],
99
+ }),
83
100
  ],
84
101
  })
@@ -0,0 +1,47 @@
1
+ import {TagIcon} from '@sanity/icons/Tag'
2
+ import {defineField} from 'sanity'
3
+
4
+ import ShopifyMetafield from '../../../components/inputs/ShopifyMetafield'
5
+ import {formatMetafieldValue} from '../../../utils/formatMetafieldValue'
6
+
7
+ export const shopifyMetafieldType = defineField({
8
+ title: 'Metafield',
9
+ name: 'shopifyMetafield',
10
+ type: 'object',
11
+ icon: TagIcon,
12
+ readOnly: true,
13
+ components: {
14
+ input: ShopifyMetafield,
15
+ },
16
+ fields: [
17
+ defineField({
18
+ name: 'namespace',
19
+ type: 'string',
20
+ }),
21
+ defineField({
22
+ name: 'key',
23
+ type: 'string',
24
+ }),
25
+ defineField({
26
+ name: 'type',
27
+ type: 'string',
28
+ }),
29
+ // `value` is deliberately left undeclared: it can be a string, number, boolean, list or object
30
+ // depending on the metafield type, and a declared field is type-checked even when hidden.
31
+ ],
32
+ preview: {
33
+ select: {
34
+ key: 'key',
35
+ namespace: 'namespace',
36
+ type: 'type',
37
+ value: 'value',
38
+ },
39
+ prepare({key, namespace, type, value}) {
40
+ const name = [namespace, key].filter(Boolean).join('.')
41
+ return {
42
+ subtitle: formatMetafieldValue(type, value),
43
+ title: type ? `${name} (${type})` : name,
44
+ }
45
+ },
46
+ },
47
+ })
@@ -29,6 +29,14 @@ export const shopifyProductType = defineField({
29
29
  collapsible: true,
30
30
  },
31
31
  },
32
+ {
33
+ name: 'metafields',
34
+ title: 'Metafields',
35
+ options: {
36
+ collapsed: true,
37
+ collapsible: true,
38
+ },
39
+ },
32
40
  ],
33
41
  fields: [
34
42
  defineField({
@@ -127,5 +135,14 @@ export const shopifyProductType = defineField({
127
135
  }),
128
136
  ],
129
137
  }),
138
+ defineField({
139
+ fieldset: 'metafields',
140
+ name: 'metafields',
141
+ type: 'array',
142
+ description: 'Read-only. Replaced in full on every sync',
143
+ // Shopify owns the order, and `readOnly` does not remove the drag handles on its own.
144
+ options: {sortable: false},
145
+ of: [defineArrayMember({type: 'shopifyMetafield'})],
146
+ }),
130
147
  ],
131
148
  })
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shows a synced Shopify metafield value as text, based on its metafield `type`.
3
+ *
4
+ * The types below are examples. Anything not listed falls back to the raw value, so your own
5
+ * metafields still display — add an entry here to show one of them nicely. Every type and the shape
6
+ * of its value: https://shopify.dev/docs/apps/build/custom-data/metafields/list-of-data-types
7
+ */
8
+ const measurement = ({value, unit}: {value: number; unit: string}) => `${value} ${unit}`
9
+
10
+ const formatters: Record<string, (value: any) => string> = {
11
+ dimension: measurement,
12
+ weight: measurement,
13
+ volume: measurement,
14
+ money: ({amount, currency_code: currencyCode}) => `${amount} ${currencyCode}`,
15
+ rating: ({value, scale_max: scaleMax}) => `${value} / ${scaleMax}`,
16
+ 'list.single_line_text_field': (values: string[]) => values.join(', '),
17
+ }
18
+
19
+ export const formatMetafieldValue = (type: string | undefined, value: unknown) => {
20
+ if (value === null || typeof value === 'undefined') {
21
+ return ''
22
+ }
23
+
24
+ const format = type ? formatters[type] : undefined
25
+ if (format) {
26
+ return format(value)
27
+ }
28
+
29
+ return typeof value === 'string' ? value : JSON.stringify(value)
30
+ }
@@ -0,0 +1,22 @@
1
+ import {Card, Code} from '@sanity/ui'
2
+ import {type ObjectInputProps} from 'sanity'
3
+ import {formatMetafieldValue} from '../../utils/formatMetafieldValue'
4
+
5
+ /**
6
+ * Shows a synced metafield's value. `value` isn't a declared field on `shopifyMetafield` — its shape
7
+ * depends on the Shopify metafield type — so it's read from the raw object value here.
8
+ */
9
+ const ShopifyMetafield = (props: ObjectInputProps) => {
10
+ const {type, value} = (props.value || {}) as {type?: string; value?: unknown}
11
+ const text = formatMetafieldValue(type, value)
12
+
13
+ return (
14
+ <Card border padding={3} radius={1} tone="transparent">
15
+ <Code size={1} style={{margin: 0}}>
16
+ {text}
17
+ </Code>
18
+ </Card>
19
+ )
20
+ }
21
+
22
+ export default ShopifyMetafield
@@ -10,9 +10,11 @@ import callout from './objects/callout'
10
10
  import inventory from './objects/inventory'
11
11
  import option from './objects/option'
12
12
  import priceRange from './objects/priceRange'
13
+ import shop from './objects/shop'
13
14
  import proxyString from './objects/proxyString'
14
15
  import shopifyCollection from './objects/shopifyCollection'
15
16
  import shopifyCollectionRule from './objects/shopifyCollectionRule'
17
+ import shopifyMetafield from './objects/shopifyMetafield'
16
18
  import shopifyProduct from './objects/shopifyProduct'
17
19
  import shopifyProductVariant from './objects/shopifyProductVariant'
18
20
 
@@ -33,8 +35,10 @@ export const schemaTypes = [
33
35
  option,
34
36
  priceRange,
35
37
  proxyString,
38
+ shop,
36
39
  shopifyCollection,
37
40
  shopifyCollectionRule,
41
+ shopifyMetafield,
38
42
  shopifyProduct,
39
43
  shopifyProductVariant,
40
44
 
@@ -0,0 +1,16 @@
1
+ import {defineField, defineType} from 'sanity'
2
+
3
+ export default defineType({
4
+ name: 'shop',
5
+ title: 'Shop',
6
+ type: 'object',
7
+ readOnly: true,
8
+ fields: [
9
+ // Domain
10
+ defineField({
11
+ name: 'domain',
12
+ title: 'Domain',
13
+ type: 'string',
14
+ }),
15
+ ],
16
+ })
@@ -14,6 +14,14 @@ export default defineType({
14
14
  name: 'status',
15
15
  title: 'Status',
16
16
  },
17
+ {
18
+ name: 'metafields',
19
+ title: 'Metafields',
20
+ options: {
21
+ collapsed: true,
22
+ collapsible: true,
23
+ },
24
+ },
17
25
  ],
18
26
  fields: [
19
27
  // Created at
@@ -98,6 +106,17 @@ export default defineType({
98
106
  title: 'Sort order',
99
107
  type: 'string',
100
108
  }),
109
+ // Metafields
110
+ defineField({
111
+ fieldset: 'metafields',
112
+ name: 'metafields',
113
+ title: 'Metafields',
114
+ type: 'array',
115
+ description: 'Read-only. Replaced in full on every sync',
116
+ // Shopify owns the order, and `readOnly` does not remove the drag handles on its own.
117
+ options: {sortable: false},
118
+ of: [{type: 'shopifyMetafield'}],
119
+ }),
101
120
  // Shop details
102
121
  defineField({
103
122
  name: 'shop',