Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-teams-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Exclude unsupported assets from the build manifest for Remote UI extensions
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('ui_extension', async () => {

async function setupToolsExtension(
tmpDir: string,
options: {tools?: string; instructions?: string; createFiles?: boolean} = {},
options: {tools?: string; instructions?: string; createFiles?: boolean; apiVersion?: string} = {},
) {
await mkdir(joinPath(tmpDir, 'src'))
await touchFile(joinPath(tmpDir, 'src', 'ExtensionPointA.js'))
Expand All @@ -106,7 +106,7 @@ describe('ui_extension', async () => {

const configuration = {
targeting: [targetConfig],
api_version: '2023-01' as const,
api_version: options.apiVersion ?? '2025-10',
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
Expand Down Expand Up @@ -557,7 +557,7 @@ describe('ui_extension', async () => {
])
})

test('build_manifest includes tools asset when tools is present', async () => {
test('build_manifest includes tools asset when tools is present and api_version supports Remote DOM', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const configuration = {
Expand All @@ -568,7 +568,7 @@ describe('ui_extension', async () => {
tools: './tools.json',
},
],
api_version: '2023-01' as const,
api_version: '2025-10' as const,
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
Expand Down Expand Up @@ -625,15 +625,15 @@ describe('ui_extension', async () => {
])
})

test('build_manifest includes instructions asset when instructions is present', async () => {
test('build_manifest excludes tools asset when api_version does not support Remote DOM', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const configuration = {
targeting: [
{
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
instructions: './instructions.md',
tools: './tools.json',
},
],
api_version: '2023-01' as const,
Expand Down Expand Up @@ -664,6 +664,69 @@ describe('ui_extension', async () => {

const got = parsed.data

// Then
expect(got.extension_points).toStrictEqual([
{
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
tools: undefined,
instructions: undefined,
metafields: [],
default_placement_reference: undefined,
capabilities: undefined,
preloads: {},
build_manifest: {
assets: {
main: {
filepath: 'test-ui-extension.js',
module: './src/ExtensionPointA.js',
},
},
},
urls: {},
},
])
})

test('build_manifest includes instructions asset when instructions is present and api_version supports Remote DOM', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const configuration = {
targeting: [
{
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
instructions: './instructions.md',
},
],
api_version: '2025-10' as const,
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
handle: 'test-ui-extension',
capabilities: {
block_progress: false,
network_access: false,
api_access: false,
collect_buyer_consent: {
customer_privacy: true,
sms_marketing: false,
},
iframe: {
sources: [],
},
},
settings: {},
}

// When
const parsed = specification.parseConfigurationObject(configuration)
if (parsed.state !== 'ok') {
throw new Error("Couldn't parse configuration")
}

const got = parsed.data

// Then
expect(got.extension_points).toStrictEqual([
{
Expand Down Expand Up @@ -693,6 +756,69 @@ describe('ui_extension', async () => {
])
})

test('build_manifest excludes instructions asset when api_version does not support Remote DOM', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const configuration = {
targeting: [
{
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
instructions: './instructions.md',
},
],
api_version: '2023-01' as const,
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
handle: 'test-ui-extension',
capabilities: {
block_progress: false,
network_access: false,
api_access: false,
collect_buyer_consent: {
customer_privacy: true,
sms_marketing: false,
},
iframe: {
sources: [],
},
},
settings: {},
}

// When
const parsed = specification.parseConfigurationObject(configuration)
if (parsed.state !== 'ok') {
throw new Error("Couldn't parse configuration")
}

const got = parsed.data

// Then
expect(got.extension_points).toStrictEqual([
{
target: 'EXTENSION::POINT::A',
module: './src/ExtensionPointA.js',
tools: undefined,
instructions: undefined,
metafields: [],
default_placement_reference: undefined,
capabilities: undefined,
preloads: {},
build_manifest: {
assets: {
main: {
filepath: 'test-ui-extension.js',
module: './src/ExtensionPointA.js',
},
},
},
urls: {},
},
])
})

test('returns error if there is no targeting or extension_points', async () => {
// Given
const allSpecs = await loadLocalExtensionsSpecifications()
Expand Down Expand Up @@ -852,7 +978,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
})
})

test('build_manifest includes both tools and instructions when both are present', async () => {
test('build_manifest includes both tools and instructions when both are present and api_version supports Remote DOM', async () => {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const configuration = {
Expand All @@ -864,7 +990,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
instructions: './instructions.md',
},
],
api_version: '2023-01' as const,
api_version: '2025-10' as const,
name: 'UI Extension',
description: 'This is an ordinary test extension',
type: 'ui_extension',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const UIExtensionSchema = BaseSchema.extend({
})
.refine((config) => validatePoints(config), missingExtensionPointsMessage)
.transform((config) => {
const apiVersion = config.api_version
const {year, month} = parseApiVersion(apiVersion ?? '') ?? {year: 0, month: 0}
const remoteDom = year > 2025 || (year === 2025 && month >= 10)

const extensionPoints = (config.targeting ?? config.extension_points ?? []).map((targeting) => {
const buildManifest: BuildManifest = {
assets: {
Expand All @@ -60,7 +64,7 @@ export const UIExtensionSchema = BaseSchema.extend({
},
}
: null),
...(targeting.tools
...(remoteDom && targeting.tools
? {
[AssetIdentifier.Tools]: {
filepath: `${config.handle}-${AssetIdentifier.Tools}-${basename(targeting.tools)}`,
Expand All @@ -69,7 +73,7 @@ export const UIExtensionSchema = BaseSchema.extend({
},
}
: null),
...(targeting.instructions
...(remoteDom && targeting.instructions
? {
[AssetIdentifier.Instructions]: {
filepath: `${config.handle}-${AssetIdentifier.Instructions}-${basename(targeting.instructions)}`,
Expand All @@ -90,8 +94,8 @@ export const UIExtensionSchema = BaseSchema.extend({
capabilities: targeting.capabilities,
preloads: targeting.preloads ?? {},
build_manifest: buildManifest,
tools: targeting.tools,
instructions: targeting.instructions,
tools: remoteDom ? targeting.tools : undefined,
instructions: remoteDom ? targeting.instructions : undefined,
}
})
return {...config, extension_points: extensionPoints}
Expand Down Expand Up @@ -424,7 +428,7 @@ async function validateUIExtensionPointConfig(

function isRemoteDomExtension(
config: ExtensionInstance['configuration'],
): config is ExtensionInstance<{api_version: string}>['configuration'] {
): config is ExtensionInstance['configuration'] & {api_version: string} {
const apiVersion = config.api_version
if (!apiVersion) {
return false
Expand Down
Loading