From 9319524114f2a690382a6857c8a0ecc1a3744ff8 Mon Sep 17 00:00:00 2001 From: js-goupil Date: Mon, 23 Feb 2026 15:03:27 -0500 Subject: [PATCH 1/3] Add runs_offline support for UI extensions Add supported_features.runs_offline configuration option for UI extensions that enables offline mode. The TOML config uses `runs_offline` but it is transformed to `offline_mode` when sent to the backend API. - Add runs_offline to SupportedFeaturesSchema - Transform runs_offline -> offline_mode in deployConfig for backend - Add offlineMode to dev server payload for local development - Add comprehensive tests for all scenarios Co-authored-by: Cursor --- packages/app/src/cli/models/extensions/schemas.ts | 2 +- .../extensions/specifications/checkout_ui_extension.ts | 7 ++++++- .../models/extensions/specifications/ui_extension.test.ts | 8 ++++---- .../cli/models/extensions/specifications/ui_extension.ts | 6 +++++- .../app/src/cli/services/dev/extension/payload.test.ts | 8 ++++---- packages/app/src/cli/services/dev/extension/payload.ts | 2 +- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/app/src/cli/models/extensions/schemas.ts b/packages/app/src/cli/models/extensions/schemas.ts index fd8a4c9dcb..cab04621ec 100644 --- a/packages/app/src/cli/models/extensions/schemas.ts +++ b/packages/app/src/cli/models/extensions/schemas.ts @@ -29,7 +29,7 @@ const CapabilitiesSchema = zod.object({ }) const SupportedFeaturesSchema = zod.object({ - offline_mode: zod.boolean().optional(), + runs_offline: zod.boolean().optional(), }) export const ExtensionsArraySchema = zod.object({ 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 f08dfd97c4..be8f736d10 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 @@ -23,10 +23,15 @@ const checkoutSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path', 'generates_source_maps'], buildConfig: {mode: 'ui'}, deployConfig: async (config, directory) => { + const supportedFeatures = + config.supported_features?.runs_offline === undefined + ? undefined + : {offline_mode: config.supported_features.runs_offline} + return { extension_points: config.extension_points, capabilities: config.capabilities, - supported_features: config.supported_features, + supported_features: supportedFeatures, 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 29a8fab843..7821b2cefe 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 @@ -996,7 +996,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` }) }) - test('returns supported_features with offline_mode true when configured', async () => { + test('returns supported_features with offline_mode true when runs_offline is configured', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) @@ -1012,7 +1012,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` metafields: [], capabilities: {}, supported_features: { - offline_mode: true, + runs_offline: true, }, settings: {}, }, @@ -1035,7 +1035,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` }) }) - test('returns supported_features with offline_mode false when configured', async () => { + test('returns supported_features with offline_mode false when runs_offline is configured', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) @@ -1051,7 +1051,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` metafields: [], capabilities: {}, supported_features: { - offline_mode: false, + runs_offline: false, }, settings: {}, }, 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 f3e04cbeab..2136a4c49a 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -115,12 +115,16 @@ const uiExtensionSpec = createExtensionSpecification({ }, deployConfig: async (config, directory) => { const transformedExtensionPoints = config.extension_points?.map(addDistPathToAssets) ?? [] + const supportedFeatures = + config.supported_features?.runs_offline === undefined + ? undefined + : {offline_mode: config.supported_features.runs_offline} return { api_version: config.api_version, extension_points: transformedExtensionPoints, capabilities: config.capabilities, - supported_features: config.supported_features, + supported_features: supportedFeatures, 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 ae905a037a..f228cbf6a3 100644 --- a/packages/app/src/cli/services/dev/extension/payload.test.ts +++ b/packages/app/src/cli/services/dev/extension/payload.test.ts @@ -462,7 +462,7 @@ describe('getUIExtensionPayload', () => { }) describe('supportedFeatures', () => { - test('returns supportedFeatures with offlineMode true when offline_mode is enabled', async () => { + test('returns supportedFeatures with offlineMode true when runs_offline is enabled', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const uiExtension = await testUIExtension({ @@ -473,7 +473,7 @@ describe('getUIExtensionPayload', () => { metafields: [], capabilities: {}, supported_features: { - offline_mode: true, + runs_offline: true, }, extension_points: [], }, @@ -493,7 +493,7 @@ describe('getUIExtensionPayload', () => { }) }) - test('returns supportedFeatures with offlineMode false when offline_mode is disabled', async () => { + test('returns supportedFeatures with offlineMode false when runs_offline is disabled', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const uiExtension = await testUIExtension({ @@ -504,7 +504,7 @@ describe('getUIExtensionPayload', () => { metafields: [], capabilities: {}, supported_features: { - offline_mode: false, + runs_offline: false, }, extension_points: [], }, diff --git a/packages/app/src/cli/services/dev/extension/payload.ts b/packages/app/src/cli/services/dev/extension/payload.ts index ed21fe7eef..ea2009385d 100644 --- a/packages/app/src/cli/services/dev/extension/payload.ts +++ b/packages/app/src/cli/services/dev/extension/payload.ts @@ -54,7 +54,7 @@ export async function getUIExtensionPayload( }, }, supportedFeatures: { - offlineMode: extension.configuration.supported_features?.offline_mode ?? false, + offlineMode: extension.configuration.supported_features?.runs_offline ?? false, }, capabilities: { blockProgress: extension.configuration.capabilities?.block_progress ?? false, From 8d9e5da4aa1225e01d5201a502bfcbbd055a7396 Mon Sep 17 00:00:00 2001 From: js-goupil Date: Mon, 23 Feb 2026 15:18:11 -0500 Subject: [PATCH 2/3] Fix test data to use runs_offline instead of offline_mode Update remaining test fixtures to use the new runs_offline field name in the configuration schema. Co-authored-by: Cursor --- packages/app/src/cli/models/app/app.test-data.ts | 2 +- packages/app/src/cli/models/app/loader.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 d44964c7a4..9ec3254e92 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -235,7 +235,7 @@ export async function testUIExtension( }, }, supported_features: { - offline_mode: false, + runs_offline: false, }, extension_points: [ { diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 9a0e4ef87f..9612b21d2d 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -1484,7 +1484,7 @@ redirect_urls = [ "https://example.com/api/auth" ] sources = ["https://my-iframe.com"] [extensions.supported_features] - offline_mode = true + runs_offline = true [extensions.settings] [[extensions.settings.fields]] @@ -1564,7 +1564,7 @@ redirect_urls = [ "https://example.com/api/auth" ] }, }, supported_features: { - offline_mode: true, + runs_offline: true, }, settings: { fields: [ @@ -3437,7 +3437,7 @@ describe('WebhooksSchema', () => { } const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have multiple subscriptions with the same compliance topic', + message: 'You can’t have multiple subscriptions with the same compliance topic', fatal: true, path: ['webhooks', 'subscriptions'], } From 631a24ebcbf1cea46fe372f5c2b7e39af329b991 Mon Sep 17 00:00:00 2001 From: js-goupil Date: Tue, 24 Feb 2026 09:51:32 -0500 Subject: [PATCH 3/3] Use runs_offline for backend API as well Backend now accepts runs_offline directly, no need to transform to offline_mode. Pass supported_features through as-is. Co-authored-by: Cursor --- packages/app/src/cli/models/app/loader.test.ts | 2 +- .../specifications/checkout_ui_extension.ts | 7 +------ .../extensions/specifications/ui_extension.test.ts | 8 ++++---- .../models/extensions/specifications/ui_extension.ts | 6 +----- .../src/cli/services/dev/extension/payload.test.ts | 12 ++++++------ .../app/src/cli/services/dev/extension/payload.ts | 2 +- .../src/cli/services/dev/extension/payload/models.ts | 2 +- packages/ui-extensions-server-kit/src/types.ts | 2 +- 8 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index 9612b21d2d..2ef672c6b7 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -3437,7 +3437,7 @@ describe('WebhooksSchema', () => { } const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have multiple subscriptions with the same compliance topic', + message: 'You can’t have multiple subscriptions with the same compliance topic', fatal: true, path: ['webhooks', 'subscriptions'], } 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 be8f736d10..f08dfd97c4 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 @@ -23,15 +23,10 @@ const checkoutSpec = createExtensionSpecification({ appModuleFeatures: (_) => ['ui_preview', 'cart_url', 'esbuild', 'single_js_entry_path', 'generates_source_maps'], buildConfig: {mode: 'ui'}, deployConfig: async (config, directory) => { - const supportedFeatures = - config.supported_features?.runs_offline === undefined - ? undefined - : {offline_mode: config.supported_features.runs_offline} - return { extension_points: config.extension_points, capabilities: config.capabilities, - supported_features: supportedFeatures, + 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 7821b2cefe..c2dfcccb1d 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 @@ -996,7 +996,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` }) }) - test('returns supported_features with offline_mode true when runs_offline is configured', async () => { + test('returns supported_features with runs_offline true when configured', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) @@ -1030,12 +1030,12 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` // Then expect(deployConfig?.supported_features).toStrictEqual({ - offline_mode: true, + runs_offline: true, }) }) }) - test('returns supported_features with offline_mode false when runs_offline is configured', async () => { + test('returns supported_features with runs_offline false when configured', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given vi.spyOn(loadLocales, 'loadLocalesConfig').mockResolvedValue({}) @@ -1069,7 +1069,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}` // Then expect(deployConfig?.supported_features).toStrictEqual({ - offline_mode: false, + runs_offline: false, }) }) }) 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 2136a4c49a..f3e04cbeab 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -115,16 +115,12 @@ const uiExtensionSpec = createExtensionSpecification({ }, deployConfig: async (config, directory) => { const transformedExtensionPoints = config.extension_points?.map(addDistPathToAssets) ?? [] - const supportedFeatures = - config.supported_features?.runs_offline === undefined - ? undefined - : {offline_mode: config.supported_features.runs_offline} return { api_version: config.api_version, extension_points: transformedExtensionPoints, capabilities: config.capabilities, - supported_features: supportedFeatures, + 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 f228cbf6a3..5fd9f08859 100644 --- a/packages/app/src/cli/services/dev/extension/payload.test.ts +++ b/packages/app/src/cli/services/dev/extension/payload.test.ts @@ -462,7 +462,7 @@ describe('getUIExtensionPayload', () => { }) describe('supportedFeatures', () => { - test('returns supportedFeatures with offlineMode true when runs_offline is enabled', async () => { + test('returns supportedFeatures with runsOffline true when runs_offline is enabled', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const uiExtension = await testUIExtension({ @@ -488,12 +488,12 @@ describe('getUIExtensionPayload', () => { // Then expect(got.supportedFeatures).toStrictEqual({ - offlineMode: true, + runsOffline: true, }) }) }) - test('returns supportedFeatures with offlineMode false when runs_offline is disabled', async () => { + test('returns supportedFeatures with runsOffline false when runs_offline is disabled', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const uiExtension = await testUIExtension({ @@ -519,12 +519,12 @@ describe('getUIExtensionPayload', () => { // Then expect(got.supportedFeatures).toStrictEqual({ - offlineMode: false, + runsOffline: false, }) }) }) - test('returns supportedFeatures with offlineMode false when supported_features is not configured', async () => { + test('returns supportedFeatures with runsOffline false when supported_features is not configured', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const uiExtension = await testUIExtension({ @@ -547,7 +547,7 @@ describe('getUIExtensionPayload', () => { // Then expect(got.supportedFeatures).toStrictEqual({ - offlineMode: false, + runsOffline: false, }) }) }) diff --git a/packages/app/src/cli/services/dev/extension/payload.ts b/packages/app/src/cli/services/dev/extension/payload.ts index ea2009385d..37d08dfbfb 100644 --- a/packages/app/src/cli/services/dev/extension/payload.ts +++ b/packages/app/src/cli/services/dev/extension/payload.ts @@ -54,7 +54,7 @@ export async function getUIExtensionPayload( }, }, supportedFeatures: { - offlineMode: extension.configuration.supported_features?.runs_offline ?? false, + runsOffline: extension.configuration.supported_features?.runs_offline ?? false, }, capabilities: { blockProgress: extension.configuration.capabilities?.block_progress ?? 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 7e617ca63a..796f93108b 100644 --- a/packages/app/src/cli/services/dev/extension/payload/models.ts +++ b/packages/app/src/cli/services/dev/extension/payload/models.ts @@ -46,7 +46,7 @@ export interface DevNewExtensionPointSchema extends NewExtensionPointSchemaType } interface SupportedFeatures { - offlineMode: boolean + runsOffline: boolean } export interface UIExtensionPayload { diff --git a/packages/ui-extensions-server-kit/src/types.ts b/packages/ui-extensions-server-kit/src/types.ts index ed27db533f..3a6503d72e 100644 --- a/packages/ui-extensions-server-kit/src/types.ts +++ b/packages/ui-extensions-server-kit/src/types.ts @@ -160,7 +160,7 @@ export interface ExtensionPayload { } export interface ExtensionSupportedFeatures { - offlineMode: boolean + runsOffline: boolean } export enum Status {