Skip to content

Commit 7692333

Browse files
authored
Merge pull request #8534 from Shopify/ui-ext-metafield-owners-in-toml
Support optional owner_type in UI extension metafields
2 parents 523961b + e754d36 commit 7692333

5 files changed

Lines changed: 104 additions & 29 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/app': minor
3+
---
4+
5+
Support owner types in UI extension metafield configuration.

‎packages/app/src/cli/models/app/loader.test.ts‎

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,7 @@ describe('load', () => {
13451345
await writeConfig(appConfiguration)
13461346

13471347
const blockConfiguration = `
1348-
api_version = "unstable"
1348+
api_version = "2026-10"
13491349
[[extensions]]
13501350
name = "my-admin-action"
13511351
handle = "admin-action-handle"
@@ -1356,6 +1356,11 @@ describe('load', () => {
13561356
[[extensions.metafields]]
13571357
namespace = "my-namespace"
13581358
key = "my-key"
1359+
owner_type = "PRODUCT"
1360+
1361+
[[extensions.metafields]]
1362+
namespace = "my-namespace"
1363+
key = "my-key-without-owner-type"
13591364
13601365
# extra fields not included in the schema should be ignored
13611366
[[extensions.invalid_field]]
@@ -1384,14 +1389,19 @@ describe('load', () => {
13841389
expect(extension).not.toBeUndefined()
13851390
if (extension) {
13861391
expect(extension.configuration).toMatchObject({
1387-
api_version: 'unstable',
1392+
api_version: '2026-10',
13881393
name: 'my-admin-action',
13891394
handle: 'admin-action-handle',
13901395
type: 'ui_extension',
13911396
metafields: [
13921397
{
13931398
namespace: 'my-namespace',
13941399
key: 'my-key',
1400+
owner_type: 'PRODUCT',
1401+
},
1402+
{
1403+
namespace: 'my-namespace',
1404+
key: 'my-key-without-owner-type',
13951405
},
13961406
],
13971407
extension_points: [
@@ -1400,6 +1410,11 @@ describe('load', () => {
14001410
{
14011411
namespace: 'my-namespace',
14021412
key: 'my-key',
1413+
owner_type: 'PRODUCT',
1414+
},
1415+
{
1416+
namespace: 'my-namespace',
1417+
key: 'my-key-without-owner-type',
14031418
},
14041419
],
14051420
module: './src/ActionExtension.js',
@@ -1421,7 +1436,7 @@ describe('load', () => {
14211436
await writeConfig(appConfiguration)
14221437

14231438
const blockConfiguration = `
1424-
api_version = "2023-07"
1439+
api_version = "2026-10"
14251440
14261441
[[extensions]]
14271442
name = "My checkout extension"
@@ -1465,6 +1480,11 @@ describe('load', () => {
14651480
target = "purchase.checkout.block.render"
14661481
module = "./CheckoutDynamicRender.jsx"
14671482
1483+
[[extensions.targeting.metafields]]
1484+
namespace = "target-namespace"
1485+
key = "target-key"
1486+
owner_type = "CART"
1487+
14681488
# extra fields not included in the schema should be ignored
14691489
[[extensions.invalid_field]]
14701490
namespace = "my-namespace"
@@ -1502,7 +1522,7 @@ describe('load', () => {
15021522
expect(extension).not.toBeUndefined()
15031523
if (extension) {
15041524
expect(extension.configuration).toMatchObject({
1505-
api_version: '2023-07',
1525+
api_version: '2026-10',
15061526
name: 'My checkout extension',
15071527
handle: 'checkout-ui',
15081528
type: 'ui_extension',
@@ -1559,12 +1579,9 @@ describe('load', () => {
15591579
{
15601580
metafields: [
15611581
{
1562-
key: 'my-key',
1563-
namespace: 'my-namespace',
1564-
},
1565-
{
1566-
key: 'my-other-key',
1567-
namespace: 'my-namespace',
1582+
key: 'target-key',
1583+
namespace: 'target-namespace',
1584+
owner_type: 'CART',
15681585
},
15691586
],
15701587
module: './CheckoutDynamicRender.jsx',
@@ -1575,6 +1592,13 @@ describe('load', () => {
15751592
{
15761593
target: 'purchase.checkout.block.render',
15771594
module: './CheckoutDynamicRender.jsx',
1595+
metafields: [
1596+
{
1597+
key: 'target-key',
1598+
namespace: 'target-namespace',
1599+
owner_type: 'CART',
1600+
},
1601+
],
15781602
},
15791603
],
15801604
})

‎packages/app/src/cli/models/extensions/schemas.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {BaseSchema, MAX_UID_LENGTH, NewExtensionPointsSchema} from './schemas.js'
1+
import {BaseSchema, MAX_UID_LENGTH, MetafieldSchema, NewExtensionPointsSchema} from './schemas.js'
22
import {describe, expect, test} from 'vitest'
33

44
const validUIDTestCases = [
@@ -56,6 +56,23 @@ describe('UIDSchema', () => {
5656
})
5757
})
5858

59+
describe('MetafieldSchema', () => {
60+
test('accepts an owner type for API validation', () => {
61+
const result = MetafieldSchema.safeParse({namespace: 'custom', key: 'value', owner_type: 'PRODUCT'})
62+
63+
expect(result).toEqual({
64+
success: true,
65+
data: {namespace: 'custom', key: 'value', owner_type: 'PRODUCT'},
66+
})
67+
})
68+
69+
test('accepts a metafield without an owner type', () => {
70+
const result = MetafieldSchema.safeParse({namespace: 'custom', key: 'value'})
71+
72+
expect(result.success).toBe(true)
73+
})
74+
})
75+
5976
describe('NewExtensionPointsSchema', () => {
6077
test.each([
6178
[

‎packages/app/src/cli/models/extensions/schemas.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ZodSchemaType<T> = zod.ZodType<T, any, any>
99
export const MetafieldSchema = zod.object({
1010
namespace: zod.string(),
1111
key: zod.string(),
12+
owner_type: zod.string().optional(),
1213
})
1314

1415
const CollectBuyerConsentCapabilitySchema = zod.object({

‎packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts‎

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('ui_extension', async () => {
4949
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
5050
const configuration = {
5151
extension_points: extensionPoints,
52-
api_version: apiVersion ?? ('2023-01' as const),
52+
api_version: apiVersion ?? '2023-01',
5353
name: 'UI Extension',
5454
description: 'This is an ordinary test extension.',
5555
type: 'ui_extension',
@@ -117,12 +117,12 @@ describe('ui_extension', async () => {
117117
},
118118
},
119119
],
120-
api_version: '2023-01' as const,
120+
api_version: '2026-10',
121121
handle: 'test-ui-extension',
122122
name: 'UI Extension',
123123
description: 'This is an ordinary test extension',
124124
type: 'ui_extension',
125-
metafields: [{namespace: 'test', key: 'test'}],
125+
metafields: [{namespace: 'test', key: 'test', owner_type: 'PRODUCT'}],
126126
capabilities: {
127127
block_progress: false,
128128
network_access: false,
@@ -155,7 +155,7 @@ describe('ui_extension', async () => {
155155
intents: undefined,
156156
assets: undefined,
157157
module: './src/ExtensionPointA.js',
158-
metafields: [{namespace: 'test', key: 'test'}],
158+
metafields: [{namespace: 'test', key: 'test', owner_type: 'PRODUCT'}],
159159
default_placement_reference: undefined,
160160
capabilities: undefined,
161161
preloads: {},
@@ -176,6 +176,34 @@ describe('ui_extension', async () => {
176176
])
177177
})
178178

179+
test('target-level metafields override extension-level metafields', async () => {
180+
const allSpecs = await loadLocalExtensionsSpecifications()
181+
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
182+
const configuration = {
183+
targeting: [
184+
{
185+
target: 'EXTENSION::POINT::A',
186+
module: './src/ExtensionPointA.js',
187+
metafields: [{namespace: 'target', key: 'value', owner_type: 'COMPANY_LOCATION'}],
188+
},
189+
],
190+
api_version: '2026-10',
191+
handle: 'test-ui-extension',
192+
name: 'UI Extension',
193+
type: 'ui_extension',
194+
metafields: [{namespace: 'extension', key: 'value', owner_type: 'SHOP'}],
195+
}
196+
197+
const parsed = specification.parseConfigurationObject(configuration)
198+
199+
expect(parsed.state).toBe('ok')
200+
if (parsed.state === 'ok') {
201+
expect(parsed.data.extension_points[0]?.metafields).toStrictEqual([
202+
{namespace: 'target', key: 'value', owner_type: 'COMPANY_LOCATION'},
203+
])
204+
}
205+
})
206+
179207
test('targeting object accepts a default_placement', async () => {
180208
const allSpecs = await loadLocalExtensionsSpecifications()
181209
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
@@ -187,7 +215,7 @@ describe('ui_extension', async () => {
187215
default_placement: 'PLACEMENT_REFERENCE1',
188216
},
189217
],
190-
api_version: '2023-01' as const,
218+
api_version: '2023-01',
191219
name: 'UI Extension',
192220
description: 'This is an ordinary test extension',
193221
type: 'ui_extension',
@@ -256,7 +284,7 @@ describe('ui_extension', async () => {
256284
capabilities: {allow_direct_linking: true, intercepts},
257285
},
258286
],
259-
api_version: '2023-01' as const,
287+
api_version: '2023-01',
260288
name: 'UI Extension',
261289
description: 'This is an ordinary test extension',
262290
type: 'ui_extension',
@@ -327,7 +355,7 @@ describe('ui_extension', async () => {
327355
preloads: {chat: '/chat', not_supported: '/hello'},
328356
},
329357
],
330-
api_version: '2023-01' as const,
358+
api_version: '2023-01',
331359
name: 'UI Extension',
332360
description: 'This is an ordinary test extension',
333361
type: 'ui_extension',
@@ -395,7 +423,7 @@ describe('ui_extension', async () => {
395423
},
396424
},
397425
],
398-
api_version: '2023-01' as const,
426+
api_version: '2023-01',
399427
name: 'UI Extension',
400428
description: 'This is an ordinary test extension',
401429
type: 'ui_extension',
@@ -465,7 +493,7 @@ describe('ui_extension', async () => {
465493
preloads: {chat: '/chat', not_supported: '/hello'},
466494
},
467495
],
468-
api_version: '2023-01' as const,
496+
api_version: '2023-01',
469497
name: 'UI Extension',
470498
description: 'This is an ordinary test extension',
471499
type: 'ui_extension',
@@ -534,7 +562,7 @@ describe('ui_extension', async () => {
534562
tools: './tools.json',
535563
},
536564
],
537-
api_version: '2023-01' as const,
565+
api_version: '2023-01',
538566
name: 'UI Extension',
539567
description: 'This is an ordinary test extension',
540568
type: 'ui_extension',
@@ -599,7 +627,7 @@ describe('ui_extension', async () => {
599627
instructions: './instructions.md',
600628
},
601629
],
602-
api_version: '2023-01' as const,
630+
api_version: '2023-01',
603631
name: 'UI Extension',
604632
description: 'This is an ordinary test extension',
605633
type: 'ui_extension',
@@ -664,7 +692,7 @@ describe('ui_extension', async () => {
664692
assets: './assets',
665693
},
666694
],
667-
api_version: '2023-01' as const,
695+
api_version: '2023-01',
668696
name: 'UI Extension',
669697
description: 'This is an ordinary test extension',
670698
type: 'ui_extension',
@@ -723,7 +751,7 @@ describe('ui_extension', async () => {
723751
const allSpecs = await loadLocalExtensionsSpecifications()
724752
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
725753
const configuration = {
726-
api_version: '2023-01' as const,
754+
api_version: '2023-01',
727755
name: 'UI Extension',
728756
description: 'This is an ordinary test extension',
729757
type: 'ui_extension',
@@ -828,7 +856,7 @@ Please check the configuration in ${uiExtension.configurationPath}`),
828856
instructions: './instructions.md',
829857
},
830858
],
831-
api_version: '2023-01' as const,
859+
api_version: '2023-01',
832860
name: 'UI Extension',
833861
description: 'This is an ordinary test extension',
834862
type: 'ui_extension',
@@ -966,7 +994,7 @@ Please check the configuration in ${uiExtension.configurationPath}`),
966994
module: './src/ExtensionPointA.js',
967995
},
968996
],
969-
api_version: '2025-10' as const,
997+
api_version: '2025-10',
970998
name: 'UI Extension',
971999
type: 'ui_extension',
9721000
handle: 'test-ui-extension',
@@ -1009,7 +1037,7 @@ Please check the configuration in ${uiExtension.configurationPath}`),
10091037
const uiExtension = new ExtensionInstance({
10101038
configuration: {
10111039
extension_points: [],
1012-
api_version: '2023-01' as const,
1040+
api_version: '2023-01',
10131041
name: 'UI Extension',
10141042
type: 'ui_extension',
10151043
metafields: [],
@@ -1048,7 +1076,7 @@ Please check the configuration in ${uiExtension.configurationPath}`),
10481076
const uiExtension = new ExtensionInstance({
10491077
configuration: {
10501078
extension_points: [],
1051-
api_version: '2023-01' as const,
1079+
api_version: '2023-01',
10521080
name: 'UI Extension',
10531081
type: 'ui_extension',
10541082
metafields: [],
@@ -1087,7 +1115,7 @@ Please check the configuration in ${uiExtension.configurationPath}`),
10871115
const uiExtension = new ExtensionInstance({
10881116
configuration: {
10891117
extension_points: [],
1090-
api_version: '2023-01' as const,
1118+
api_version: '2023-01',
10911119
name: 'UI Extension',
10921120
type: 'ui_extension',
10931121
metafields: [],

0 commit comments

Comments
 (0)