From f903c4780cb978b68826001d5a8598969a373ec3 Mon Sep 17 00:00:00 2001 From: js-goupil Date: Thu, 15 Jan 2026 11:58:08 -0500 Subject: [PATCH] Added support for supported features in toml --- .changeset/short-cooks-wonder.md | 6 + .../app/src/cli/models/app/app.test-data.ts | 3 + .../app/src/cli/models/app/loader.test.ts | 6 + .../app/src/cli/models/extensions/schemas.ts | 5 + .../specifications/checkout_ui_extension.ts | 1 + .../specifications/ui_extension.test.ts | 113 ++++++++++++++++++ .../extensions/specifications/ui_extension.ts | 1 + .../services/dev/extension/payload.test.ts | 92 ++++++++++++++ .../src/cli/services/dev/extension/payload.ts | 3 + .../services/dev/extension/payload/models.ts | 5 + 10 files changed, 235 insertions(+) create mode 100644 .changeset/short-cooks-wonder.md diff --git a/.changeset/short-cooks-wonder.md b/.changeset/short-cooks-wonder.md new file mode 100644 index 00000000000..76222c7bc21 --- /dev/null +++ b/.changeset/short-cooks-wonder.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-kit': minor +'@shopify/app': minor +--- + +Added CLI support for extensions.supported_features in toml diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 4049fd80a59..d44964c7a40 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -234,6 +234,9 @@ export async function testUIExtension( sources: [], }, }, + supported_features: { + offline_mode: false, + }, extension_points: [ { target: 'target1', diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 42cf61e9958..bd7f91450b1 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -1483,6 +1483,9 @@ redirect_urls = [ "https://example.com/api/auth" ] [extensions.capabilities.iframe] sources = ["https://my-iframe.com"] + [extensions.supported_features] + offline_mode = true + [extensions.settings] [[extensions.settings.fields]] key = "field_key" @@ -1560,6 +1563,9 @@ redirect_urls = [ "https://example.com/api/auth" ] sources: ['https://my-iframe.com'], }, }, + supported_features: { + offline_mode: true, + }, settings: { fields: [ { diff --git a/packages/app/src/cli/models/extensions/schemas.ts b/packages/app/src/cli/models/extensions/schemas.ts index 5fe195ff3e4..fd8a4c9dcbc 100644 --- a/packages/app/src/cli/models/extensions/schemas.ts +++ b/packages/app/src/cli/models/extensions/schemas.ts @@ -28,6 +28,10 @@ const CapabilitiesSchema = zod.object({ iframe: IframeCapabilitySchema.optional(), }) +const SupportedFeaturesSchema = zod.object({ + offline_mode: zod.boolean().optional(), +}) + export const ExtensionsArraySchema = zod.object({ type: zod.string().optional(), extensions: zod.array(zod.any()).optional(), @@ -108,6 +112,7 @@ export const BaseSchema = zod.object({ api_version: ApiVersionSchema.optional(), extension_points: zod.any().optional(), capabilities: CapabilitiesSchema.optional(), + supported_features: SupportedFeaturesSchema.optional(), settings: SettingsSchema.optional(), }) diff --git a/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts index 39fc3b598a7..f08dfd97c40 100644 --- a/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/checkout_ui_extension.ts @@ -26,6 +26,7 @@ const checkoutSpec = createExtensionSpecification({ return { extension_points: config.extension_points, capabilities: config.capabilities, + supported_features: config.supported_features, metafields: config.metafields ?? [], name: config.name, settings: config.settings, diff --git a/packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts b/packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts index 4494db13efd..1e681af82bf 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts @@ -978,6 +978,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` ...uiExtension.configuration.capabilities.iframe, }, }, + supported_features: undefined, name: uiExtension.configuration.name, description: uiExtension.configuration.description, api_version: uiExtension.configuration.api_version, @@ -985,6 +986,118 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` }) }) }) + + test('returns supported_features with offline_mode true when configured', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) + const configurationPath = joinPath(tmpDir, 'shopify.extension.toml') + const allSpecs = await loadLocalExtensionsSpecifications() + const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')! + const uiExtension = new ExtensionInstance({ + configuration: { + extension_points: [], + api_version: '2023-01' as const, + name: 'UI Extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + supported_features: { + offline_mode: true, + }, + settings: {}, + }, + directory: tmpDir, + specification, + configurationPath, + entryPath: '', + }) + + // When + const deployConfig = await uiExtension.deployConfig({ + apiKey: 'apiKey', + appConfiguration: placeholderAppConfiguration, + }) + + // Then + expect(deployConfig?.supported_features).toStrictEqual({ + offline_mode: true, + }) + }) + }) + + test('returns supported_features with offline_mode false when configured', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) + const configurationPath = joinPath(tmpDir, 'shopify.extension.toml') + const allSpecs = await loadLocalExtensionsSpecifications() + const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')! + const uiExtension = new ExtensionInstance({ + configuration: { + extension_points: [], + api_version: '2023-01' as const, + name: 'UI Extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + supported_features: { + offline_mode: false, + }, + settings: {}, + }, + directory: tmpDir, + specification, + configurationPath, + entryPath: '', + }) + + // When + const deployConfig = await uiExtension.deployConfig({ + apiKey: 'apiKey', + appConfiguration: placeholderAppConfiguration, + }) + + // Then + expect(deployConfig?.supported_features).toStrictEqual({ + offline_mode: false, + }) + }) + }) + + test('returns supported_features as undefined when not configured', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) + const configurationPath = joinPath(tmpDir, 'shopify.extension.toml') + const allSpecs = await loadLocalExtensionsSpecifications() + const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')! + const uiExtension = new ExtensionInstance({ + configuration: { + extension_points: [], + api_version: '2023-01' as const, + name: 'UI Extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + settings: {}, + }, + directory: tmpDir, + specification, + configurationPath, + entryPath: '', + }) + + // When + const deployConfig = await uiExtension.deployConfig({ + apiKey: 'apiKey', + appConfiguration: placeholderAppConfiguration, + }) + + // Then + expect(deployConfig?.supported_features).toBeUndefined() + }) + }) }) describe('getBundleExtensionStdinContent()', async () => { diff --git a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts index 0aea517ccf7..a0d0df25f30 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -125,6 +125,7 @@ const uiExtensionSpec = createExtensionSpecification({ api_version: config.api_version, extension_points: transformedExtensionPoints, capabilities: config.capabilities, + supported_features: config.supported_features, name: config.name, description: config.description, settings: config.settings, diff --git a/packages/app/src/cli/services/dev/extension/payload.test.ts b/packages/app/src/cli/services/dev/extension/payload.test.ts index 44c649fc593..d8080d60a8c 100644 --- a/packages/app/src/cli/services/dev/extension/payload.test.ts +++ b/packages/app/src/cli/services/dev/extension/payload.test.ts @@ -421,6 +421,98 @@ describe('getUIExtensionPayload', () => { }) }) + describe('supportedFeatures', () => { + test('returns supportedFeatures with offlineMode true when offline_mode is enabled', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const uiExtension = await testUIExtension({ + directory: tmpDir, + configuration: { + name: 'test-extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + supported_features: { + offline_mode: true, + }, + extension_points: [], + }, + }) + const options: ExtensionsPayloadStoreOptions = {} as ExtensionsPayloadStoreOptions + + // When + const got = await getUIExtensionPayload(uiExtension, 'mock-bundle-path', { + ...options, + currentDevelopmentPayload: {}, + }) + + // Then + expect(got.supportedFeatures).toStrictEqual({ + offlineMode: true, + }) + }) + }) + + test('returns supportedFeatures with offlineMode false when offline_mode is disabled', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const uiExtension = await testUIExtension({ + directory: tmpDir, + configuration: { + name: 'test-extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + supported_features: { + offline_mode: false, + }, + extension_points: [], + }, + }) + const options: ExtensionsPayloadStoreOptions = {} as ExtensionsPayloadStoreOptions + + // When + const got = await getUIExtensionPayload(uiExtension, 'mock-bundle-path', { + ...options, + currentDevelopmentPayload: {}, + }) + + // Then + expect(got.supportedFeatures).toStrictEqual({ + offlineMode: false, + }) + }) + }) + + test('returns supportedFeatures with offlineMode false when supported_features is not configured', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const uiExtension = await testUIExtension({ + directory: tmpDir, + configuration: { + name: 'test-extension', + type: 'ui_extension', + metafields: [], + capabilities: {}, + extension_points: [], + }, + }) + const options: ExtensionsPayloadStoreOptions = {} as ExtensionsPayloadStoreOptions + + // When + const got = await getUIExtensionPayload(uiExtension, 'mock-bundle-path', { + ...options, + currentDevelopmentPayload: {}, + }) + + // Then + expect(got.supportedFeatures).toStrictEqual({ + offlineMode: false, + }) + }) + }) + }) + test('adds root.url, resource.url and surface to extensionPoints[n] when extensionPoints[n] is an object', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given diff --git a/packages/app/src/cli/services/dev/extension/payload.ts b/packages/app/src/cli/services/dev/extension/payload.ts index 57f9ae3433f..faa7ae2628c 100644 --- a/packages/app/src/cli/services/dev/extension/payload.ts +++ b/packages/app/src/cli/services/dev/extension/payload.ts @@ -44,6 +44,9 @@ export async function getUIExtensionPayload( lastUpdated: (await fileLastUpdatedTimestamp(extensionOutputPath)) ?? 0, }, }, + supportedFeatures: { + offlineMode: extension.configuration.supported_features?.offline_mode ?? false, + }, capabilities: { blockProgress: extension.configuration.capabilities?.block_progress ?? false, networkAccess: extension.configuration.capabilities?.network_access ?? false, diff --git a/packages/app/src/cli/services/dev/extension/payload/models.ts b/packages/app/src/cli/services/dev/extension/payload/models.ts index dbf0ba53169..0adfcc22c30 100644 --- a/packages/app/src/cli/services/dev/extension/payload/models.ts +++ b/packages/app/src/cli/services/dev/extension/payload/models.ts @@ -45,10 +45,15 @@ export interface DevNewExtensionPointSchema extends NewExtensionPointSchemaType } } +interface SupportedFeatures { + offlineMode: boolean +} + export interface UIExtensionPayload { assets: { main: Asset } + supportedFeatures?: SupportedFeatures capabilities?: Capabilities development: { resource: {