From f6986233193d8d3b4322ae24f12ce9b6d3af99b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 7 Jul 2026 10:48:43 +0200 Subject: [PATCH 1/3] Remove deprecated draftable extension process Assisted-By: devx/782ef931-ac0a-4cc4-bff1-a2511c379664 --- .../remove-draftable-extension-process.md | 5 + .../partners/generated/update-draft.ts | 117 ----- .../partners/queries/update-draft.graphql | 16 - .../app/src/cli/models/app/app.test-data.ts | 11 - .../dev/processes/draftable-extension.ts | 112 ----- .../dev/processes/setup-dev-processes.test.ts | 138 +----- .../dev/processes/setup-dev-processes.ts | 11 +- .../cli/services/dev/update-extension.test.ts | 447 ------------------ .../src/cli/services/dev/update-extension.ts | 98 ---- .../utilities/developer-platform-client.ts | 5 - .../app-management-client.ts | 8 - .../partners-client.ts | 9 - 12 files changed, 9 insertions(+), 968 deletions(-) create mode 100644 .changeset/remove-draftable-extension-process.md delete mode 100644 packages/app/src/cli/api/graphql/partners/generated/update-draft.ts delete mode 100644 packages/app/src/cli/api/graphql/partners/queries/update-draft.graphql delete mode 100644 packages/app/src/cli/services/dev/processes/draftable-extension.ts delete mode 100644 packages/app/src/cli/services/dev/update-extension.test.ts delete mode 100644 packages/app/src/cli/services/dev/update-extension.ts diff --git a/.changeset/remove-draftable-extension-process.md b/.changeset/remove-draftable-extension-process.md new file mode 100644 index 00000000000..e10bd077856 --- /dev/null +++ b/.changeset/remove-draftable-extension-process.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Remove deprecated draftable extension updates from app dev diff --git a/packages/app/src/cli/api/graphql/partners/generated/update-draft.ts b/packages/app/src/cli/api/graphql/partners/generated/update-draft.ts deleted file mode 100644 index 0d4f2e9302a..00000000000 --- a/packages/app/src/cli/api/graphql/partners/generated/update-draft.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -import * as Types from './types.js' - -import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' - -export type ExtensionUpdateDraftMutationVariables = Types.Exact<{ - apiKey: Types.Scalars['String']['input'] - registrationId: Types.Scalars['ID']['input'] - config: Types.Scalars['JSON']['input'] - context?: Types.InputMaybe - handle?: Types.InputMaybe -}> - -export type ExtensionUpdateDraftMutation = { - extensionUpdateDraft?: {userErrors?: {field?: string[] | null; message: string}[] | null} | null -} - -export const ExtensionUpdateDraft = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'mutation', - name: {kind: 'Name', value: 'ExtensionUpdateDraft'}, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'apiKey'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, - }, - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'registrationId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, - }, - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'config'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'JSON'}}}, - }, - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'context'}}, - type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, - }, - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'handle'}}, - type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'extensionUpdateDraft'}, - arguments: [ - { - kind: 'Argument', - name: {kind: 'Name', value: 'input'}, - value: { - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: {kind: 'Name', value: 'apiKey'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'apiKey'}}, - }, - { - kind: 'ObjectField', - name: {kind: 'Name', value: 'registrationId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'registrationId'}}, - }, - { - kind: 'ObjectField', - name: {kind: 'Name', value: 'config'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'config'}}, - }, - { - kind: 'ObjectField', - name: {kind: 'Name', value: 'context'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'context'}}, - }, - { - kind: 'ObjectField', - name: {kind: 'Name', value: 'handle'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'handle'}}, - }, - ], - }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'userErrors'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'field'}}, - {kind: 'Field', name: {kind: 'Name', value: 'message'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/partners/queries/update-draft.graphql b/packages/app/src/cli/api/graphql/partners/queries/update-draft.graphql deleted file mode 100644 index fde5ff2b525..00000000000 --- a/packages/app/src/cli/api/graphql/partners/queries/update-draft.graphql +++ /dev/null @@ -1,16 +0,0 @@ -mutation ExtensionUpdateDraft( - $apiKey: String! - $registrationId: ID! - $config: JSON! - $context: String - $handle: String -) { - extensionUpdateDraft( - input: {apiKey: $apiKey, registrationId: $registrationId, config: $config, context: $context, handle: $handle} - ) { - userErrors { - field - message - } - } -} 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 5d7eb1fb90e..2104a8f687a 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -67,10 +67,6 @@ import {MigrateAppModuleSchema, MigrateAppModuleVariables} from '../../api/graph import appWebhookSubscriptionSpec from '../extensions/specifications/app_config_webhook_subscription.js' import appAccessSpec from '../extensions/specifications/app_config_app_access.js' import {AppLogsSubscribeResponse} from '../../api/graphql/subscribe_to_app_logs.js' -import { - ExtensionUpdateDraftMutation, - ExtensionUpdateDraftMutationVariables, -} from '../../api/graphql/partners/generated/update-draft.js' import {SchemaDefinitionByTargetQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-target.js' import {SchemaDefinitionByApiTypeQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js' import {AppHomeSpecIdentifier} from '../extensions/specifications/app_config_app_home.js' @@ -1239,12 +1235,6 @@ export const extensionCreateResponse: ExtensionCreateSchema = { }, } -const extensionUpdateResponse: ExtensionUpdateDraftMutation = { - extensionUpdateDraft: { - userErrors: [], - }, -} - const deployResponse: AppDeploySchema = { appDeploy: { appVersion: { @@ -1391,7 +1381,6 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(appVersionByTagResponse), appVersionsDiff: (_input: AppVersionsDiffVariables) => Promise.resolve(appVersionsDiffResponse), createExtension: (_input: ExtensionCreateVariables) => Promise.resolve(extensionCreateResponse), - updateExtension: (_input: ExtensionUpdateDraftMutationVariables) => Promise.resolve(extensionUpdateResponse), deploy: (_input: AppDeployVariables) => Promise.resolve(deployResponse), release: (_input: {app: MinimalAppIdentifiers; version: AppVersionIdentifiers}) => Promise.resolve(releaseResponse), generateSignedUploadUrl: (_app: MinimalAppIdentifiers) => Promise.resolve(generateSignedUploadUrlResponse), diff --git a/packages/app/src/cli/services/dev/processes/draftable-extension.ts b/packages/app/src/cli/services/dev/processes/draftable-extension.ts deleted file mode 100644 index a31ed3a11af..00000000000 --- a/packages/app/src/cli/services/dev/processes/draftable-extension.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {BaseProcess, DevProcessFunction} from './types.js' -import {updateExtensionDraft} from '../update-extension.js' -import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' -import {AppInterface} from '../../../models/app/app.js' -import {PartnersAppForIdentifierMatching, ensureDeploymentIdsPresence} from '../../context/identifiers.js' -import {getAppIdentifiers} from '../../../models/app/identifiers.js' -import {installJavy} from '../../function/build.js' -import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' -import {AppEvent, AppEventWatcher, EventType} from '../app-events/app-event-watcher.js' -import {AbortError} from '@shopify/cli-kit/node/error' -import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components' - -interface DraftableExtensionOptions { - extensions: ExtensionInstance[] - developerPlatformClient: DeveloperPlatformClient - apiKey: string - remoteExtensionIds: {[key: string]: string} - proxyUrl: string - localApp: AppInterface - appWatcher: AppEventWatcher -} - -export interface DraftableExtensionProcess extends BaseProcess { - type: 'draftable-extension' -} - -export const pushUpdatesForDraftableExtensions: DevProcessFunction = async ( - {stderr, stdout}, - {developerPlatformClient, apiKey, remoteExtensionIds: remoteExtensions, localApp: app, appWatcher}, -) => { - // Force the download of the javy binary in advance to avoid later problems, - // as it might be done multiple times in parallel. https://github.com/Shopify/cli/issues/2877 - await installJavy(app) - - const draftableExtensions = app.draftableExtensions.map((ext) => ext.handle) - - const handleAppEvent = async (event: AppEvent) => { - const extensionEvents = event.extensionEvents - .filter((ev) => ev.type === EventType.Updated) - .filter((ev) => ev.buildResult?.status === 'ok') - .filter((ev) => draftableExtensions.includes(ev.extension.handle)) - - const promises = extensionEvents.map(async (extensionEvent) => { - const extension = extensionEvent.extension - const registrationId = remoteExtensions[extension.localIdentifier] - if (!registrationId) throw new AbortError(`Extension ${extension.localIdentifier} not found on remote app.`) - await useConcurrentOutputContext({outputPrefix: extension.outputPrefix}, async () => { - await updateExtensionDraft({ - extension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: app.configuration, - bundlePath: appWatcher.buildOutputPath, - }) - }) - }) - await Promise.all(promises) - } - - appWatcher.onEvent(handleAppEvent).onStart(handleAppEvent) -} - -export async function setupDraftableExtensionsProcess({ - localApp, - apiKey, - developerPlatformClient, - remoteApp, - ...options -}: Omit & { - remoteApp: PartnersAppForIdentifierMatching -}): Promise { - const draftableExtensions = localApp.draftableExtensions - if (draftableExtensions.length === 0) { - return - } - - const prodEnvIdentifiers = getAppIdentifiers({app: localApp}) - - const {extensionIds: remoteExtensionIds, extensions: extensionsUuids} = await ensureDeploymentIdsPresence({ - app: localApp, - remoteApp, - appId: apiKey, - appName: remoteApp.title, - force: true, - release: true, - developerPlatformClient, - envIdentifiers: prodEnvIdentifiers, - includeDraftExtensions: true, - }) - - // Update the local app with the remote extension UUIDs. - // Extensions are initialized with a random dev UUID when running the dev command - // which is sent over WS messages for live reload in dev preview of UI Extensions. - localApp.updateExtensionUUIDS(extensionsUuids) - - return { - type: 'draftable-extension', - prefix: 'extensions', - function: pushUpdatesForDraftableExtensions, - options: { - localApp, - apiKey, - developerPlatformClient, - ...options, - extensions: draftableExtensions, - remoteExtensionIds, - }, - } -} diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts index c396448bd7e..ee33c9a711c 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.test.ts @@ -4,21 +4,16 @@ import {sendWebhook} from './uninstall-webhook.js' import {WebProcess, launchWebProcess} from './web.js' import {PreviewableExtensionProcess, launchPreviewableExtensionProcess} from './previewable-extension.js' import {launchGraphiQLServer} from './graphiql.js' -import {pushUpdatesForDraftableExtensions} from './draftable-extension.js' import {pushUpdatesForDevSession} from './dev-session/dev-session-process.js' import {runThemeAppExtensionsServer} from './theme-app-extension.js' import {launchAppWatcher} from './app-watcher-process.js' import { - testAppAccessConfigExtension, - testAppConfigExtensions, testAppWithConfig, testDeveloperPlatformClient, - testSingleWebhookSubscriptionExtension, testTaxCalculationExtension, testThemeExtensions, testUIExtension, testFunctionExtension, - testWebhookExtensions, testOrganizationApp, testAppLinked, testOrganization, @@ -26,7 +21,6 @@ import { testProject, } from '../../../models/app/app.test-data.js' import {WebType} from '../../../models/app/app.js' -import {ensureDeploymentIdsPresence} from '../../context/identifiers.js' import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' import {AppEventWatcher} from '../app-events/app-event-watcher.js' import * as loader from '../../../models/app/loader.js' @@ -39,7 +33,6 @@ import {isStorefrontPasswordProtected} from '@shopify/theme' import {fetchTheme} from '@shopify/cli-kit/node/themes/api' import {adminFqdn} from '@shopify/cli-kit/node/context/fqdn' -vi.mock('../../context/identifiers.js') vi.mock('@shopify/cli-kit/node/session.js') vi.mock('../fetch.js') vi.mock('@shopify/cli-kit/node/environment') @@ -54,14 +47,6 @@ vi.mock('@shopify/cli-kit/node/context/fqdn', async (importOriginal) => { }) beforeEach(() => { - // mocked for draft extensions - vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue({ - extensionIds: {}, - app: 'app-id', - extensions: {}, - extensionsNonUuidManaged: {}, - }) - // mocked for theme app extensions vi.mocked(ensureAuthenticatedAdmin).mockResolvedValue({ storeFqdn: 'store.myshopify.io', @@ -229,19 +214,6 @@ describe('setup-dev-processes', () => { }, }) expect(res.processes[3]).toMatchObject({ - type: 'draftable-extension', - prefix: 'extensions', - function: pushUpdatesForDraftableExtensions, - options: { - localApp, - apiKey: 'api-key', - developerPlatformClient, - extensions: expect.arrayContaining([draftable]), - remoteExtensionIds: {}, - proxyUrl: 'https://example.com/proxy', - }, - }) - expect(res.processes[4]).toMatchObject({ type: 'theme-app-extensions', prefix: 'theme-extensions', function: runThemeAppExtensionsServer, @@ -261,7 +233,7 @@ describe('setup-dev-processes', () => { themeExtensionPort: 9293, }, }) - expect(res.processes[5]).toMatchObject({ + expect(res.processes[4]).toMatchObject({ type: 'send-webhook', prefix: 'webhooks', function: sendWebhook, @@ -524,7 +496,8 @@ describe('setup-dev-processes', () => { graphiqlKey, }) - expect(res.processes[6]).toMatchObject({ + const appLogsProcess = res.processes.find((process) => process.type === 'app-logs-subscribe') + expect(appLogsProcess).toMatchObject({ type: 'app-logs-subscribe', prefix: 'app-logs', function: subscribeAndStartPolling, @@ -626,109 +599,4 @@ describe('setup-dev-processes', () => { expect(logsProcess?.options).toHaveProperty('localApp') expect(logsProcess?.options).toHaveProperty('appWatcher') }) - - test('pushUpdatesForDraftableExtensions does not include config extensions except app_access', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - const storeFqdn = 'store.myshopify.io' - const storeId = '123456789' - const remoteAppUpdated = true - const graphiqlPort = 1234 - const commandOptions: DevConfig['commandOptions'] = { - ...appContextResult, - subscriptionProductUrl: '/products/999999', - checkoutCartUrl: '/cart/999999:1', - theme: '1', - directory: '', - update: false, - commandConfig: new Config({root: ''}), - skipDependenciesInstallation: false, - tunnel: {mode: 'auto'}, - } - const network: DevConfig['network'] = { - proxyUrl: 'https://example.com/proxy', - proxyPort: 444, - backendPort: 111, - frontendPort: 222, - currentUrls: { - applicationUrl: 'https://example.com/application', - redirectUrlWhitelist: ['https://example.com/redirect'], - }, - } - const previewable = await testUIExtension({type: 'checkout_ui_extension'}) - const draftable = await testTaxCalculationExtension() - const nonDraftableSingleUidStrategyExtension = await testAppConfigExtensions() - const draftableSingleUidStrategyExtension = await testAppAccessConfigExtension() - const webhookSubscriptionModuleExtension = await testSingleWebhookSubscriptionExtension() - const webhooksModuleExtension = await testWebhookExtensions() - const theme = await testThemeExtensions() - const localApp = testAppWithConfig({ - config: {}, - app: { - webs: [ - { - directory: 'web', - configuration: { - roles: [WebType.Backend, WebType.Frontend], - commands: {dev: 'npm exec remix dev'}, - webhooks_path: '/webhooks', - hmr_server: { - http_paths: ['/ping'], - }, - }, - }, - ], - allExtensions: [ - previewable, - draftable, - theme, - nonDraftableSingleUidStrategyExtension, - draftableSingleUidStrategyExtension, - webhookSubscriptionModuleExtension, - webhooksModuleExtension, - ], - }, - }) - vi.spyOn(loader, 'reloadApp').mockResolvedValue(localApp) - - const remoteApp: DevConfig['remoteApp'] = { - apiKey: 'api-key', - apiSecretKeys: [{secret: 'api-secret'}], - id: '1234', - title: 'App', - organizationId: '5678', - grantedScopes: [], - flags: [], - developerPlatformClient, - } - - const graphiqlKey = 'somekey' - - const res = await setupDevProcesses({ - localApp, - commandOptions, - network, - remoteApp, - remoteAppUpdated, - storeFqdn, - storeId, - developerPlatformClient, - partnerUrlsUpdated: true, - graphiqlPort, - graphiqlKey, - }) - - expect(res.processes[3]).toMatchObject({ - type: 'draftable-extension', - prefix: 'extensions', - function: pushUpdatesForDraftableExtensions, - options: { - localApp, - apiKey: 'api-key', - developerPlatformClient, - extensions: expect.arrayContaining([draftable, theme, previewable, draftableSingleUidStrategyExtension]), - remoteExtensionIds: {}, - proxyUrl: 'https://example.com/proxy', - }, - }) - }) }) diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index 04a78e30c96..8fe5b460bff 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -1,7 +1,6 @@ import {BaseProcess, DevProcessFunction} from './types.js' import {PreviewThemeAppExtensionsProcess, setupPreviewThemeAppExtensionsProcess} from './theme-app-extension.js' import {PreviewableExtensionProcess, setupPreviewableExtensionsProcess} from './previewable-extension.js' -import {DraftableExtensionProcess, setupDraftableExtensionsProcess} from './draftable-extension.js' import {SendWebhookProcess, setupSendUninstallWebhookProcess} from './uninstall-webhook.js' import {GraphiQLServerProcess, setupGraphiQLServerProcess} from './graphiql.js' import {WebProcess, setupWebProcesses} from './web.js' @@ -41,7 +40,6 @@ type DevProcessDefinition = | WebProcess | ProxyServerProcess | PreviewableExtensionProcess - | DraftableExtensionProcess | GraphiQLServerProcess | DevSessionProcess | AppLogsSubscribeProcess @@ -166,14 +164,7 @@ export async function setupDevProcesses({ appPreviewURL: appPreviewUrl, devSessionStatusManager, }) - : await setupDraftableExtensionsProcess({ - localApp: reloadedApp, - remoteApp, - apiKey, - developerPlatformClient, - proxyUrl: network.proxyUrl, - appWatcher, - }), + : undefined, await setupPreviewThemeAppExtensionsProcess({ remoteApp, localApp: reloadedApp, diff --git a/packages/app/src/cli/services/dev/update-extension.test.ts b/packages/app/src/cli/services/dev/update-extension.test.ts deleted file mode 100644 index 67304d3b8b7..00000000000 --- a/packages/app/src/cli/services/dev/update-extension.test.ts +++ /dev/null @@ -1,447 +0,0 @@ -import {updateExtensionDraft} from './update-extension.js' -import { - placeholderAppConfiguration, - testFunctionExtension, - testDeveloperPlatformClient, - testPaymentExtensions, - testThemeExtensions, - testUIExtension, -} from '../../models/app/app.test-data.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {ExtensionUpdateDraftMutationVariables} from '../../api/graphql/partners/generated/update-draft.js' -import {inTemporaryDirectory, mkdir, writeFile} from '@shopify/cli-kit/node/fs' -import {outputInfo} from '@shopify/cli-kit/node/output' -import {describe, expect, vi, test} from 'vitest' -import {dirname, joinPath} from '@shopify/cli-kit/node/path' -import {randomUUID} from '@shopify/cli-kit/node/crypto' - -vi.mock('@shopify/cli-kit/node/crypto') -vi.mock('@shopify/cli-kit/node/output') - -const apiKey = 'mock-api-key' -const registrationId = 'mock-registration-id' -const handle = 'mock-handle' -const stdout = {write: vi.fn()} as any -const stderr = {write: vi.fn()} as any - -describe('updateExtensionDraft()', () => { - test('updates draft successfully and outputs debug message', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - await inTemporaryDirectory(async (tmpDir) => { - const configuration = { - runtime_context: 'strict', - settings: {type: 'object'}, - type: 'web_pixel_extension', - handle, - uid: 'uid1', - } as any - - const mockExtension = await testUIExtension({ - devUUID: '1', - configuration, - directory: tmpDir, - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ - apiKey, - context: '', - handle, - registrationId, - config: - '{"runtime_context":"strict","runtime_configuration_definition":{"type":"object"},"serialized_script":"dGVzdCBjb250ZW50"}', - }) - - // Check if outputDebug is called with success message - expect(outputInfo).toHaveBeenCalledWith( - `Draft updated successfully for extension: ${mockExtension.localIdentifier}`, - stdout, - ) - }) - }) - - test('updates draft successfully with context for extension with target', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - const mockExtension = await testPaymentExtensions() - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: 'dir', - }) - - expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ - apiKey, - context: 'payments.offsite.render', - handle: mockExtension.handle, - registrationId, - config: '{}', - }) - - // Check if outputDebug is called with success message - expect(outputInfo).toHaveBeenCalledWith( - `Draft updated successfully for extension: ${mockExtension.localIdentifier}`, - stdout, - ) - }) - - test('updates draft successfully when extension doesnt support esbuild', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - await inTemporaryDirectory(async (tmpDir) => { - const configuration = { - production_api_base_url: 'url1', - benchmark_api_base_url: 'url2', - type: 'tax_calculation', - handle, - } as any - - const mockExtension = await testUIExtension({ - devUUID: '1', - configuration, - directory: tmpDir, - }) - - await mkdir(joinPath(tmpDir, 'dist')) - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ - apiKey, - context: '', - handle, - registrationId, - config: '{"production_api_base_url":"url1","benchmark_api_base_url":"url2"}', - }) - - // Check if outputDebug is called with success message - expect(outputInfo).toHaveBeenCalledWith( - `Draft updated successfully for extension: ${mockExtension.localIdentifier}`, - stdout, - ) - }) - }) - - test('updates draft successfully for theme app extension', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testThemeExtensions(tmpDir) - - const filepath = 'blocks/block1.liquid' - const content = 'test content' - const base64Content = Buffer.from(content).toString('base64') - await mkdir(joinPath(tmpDir, 'blocks')) - await writeFile(joinPath(tmpDir, filepath), content) - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ - apiKey, - context: '', - handle: mockExtension.handle, - registrationId, - config: JSON.stringify({ - theme_extension: { - files: {[filepath]: base64Content}, - }, - }), - }) - }) - }) - - test('updates draft successfully for function app extension', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient() - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testFunctionExtension({dir: tmpDir}) - const moduleId = 'moduleId' - - vi.mocked(randomUUID).mockReturnValue(moduleId) - - const filepath = 'index.wasm' - const content = 'test content' - const base64Content = Buffer.from(content).toString('base64') - await mkdir(joinPath(mockExtension.directory, 'dist')) - const outputPath = mockExtension.outputPath - await mkdir(dirname(outputPath)) - await writeFile(outputPath, content) - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(developerPlatformClient.updateExtension).toHaveBeenCalledWith({ - apiKey, - context: '', - handle: mockExtension.handle, - registrationId, - config: JSON.stringify({ - title: 'test function extension', - module_id: moduleId, - description: 'description', - app_key: 'mock-api-key', - api_type: 'product_discounts', - api_version: '2022-07', - enable_creation_ui: true, - localization: {}, - uploaded_files: {'dist/index.wasm': base64Content}, - }), - }) - }) - }) - - test('handles user errors with stderr message', async () => { - const errorResponse = { - extensionUpdateDraft: { - clientMutationId: 'client-mutation-id', - userErrors: [ - {field: ['field'], message: 'Error1'}, - {field: ['field'], message: 'Error2'}, - ], - }, - } - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.resolve(errorResponse), - }) - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, mockExtension.uid, 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith('Error updating extension draft for test-ui-extension: Error1, Error2') - }) - }) - - test('handles system error with errors array', async () => { - const systemError = { - errors: [{message: 'Network error'}, {message: 'Timeout error'}], - } - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.reject(systemError), // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors -- testing non-Error rejection handling, - }) - - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith( - 'Error updating extension draft for test-ui-extension: Network error, Timeout error', - ) - }) - }) - - test('handles system error with message string', async () => { - const systemError = {message: 'API connection failed'} - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.reject(systemError), // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors -- testing non-Error rejection handling, - }) - - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith( - 'Error updating extension draft for test-ui-extension: API connection failed', - ) - }) - }) - - test('handles string error', async () => { - const systemError = 'Connection timeout' - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.reject(systemError), // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors -- testing non-Error rejection handling, - }) - - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith( - 'Error updating extension draft for test-ui-extension: Connection timeout', - ) - }) - }) - - test('handles null/undefined error with fallback message', async () => { - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.reject(null), // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors -- testing null rejection handling, - }) - - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith('Error updating extension draft for test-ui-extension: Unknown error') - }) - }) - - test('handles object error without errors or message properties', async () => { - const systemError = {status: 500, code: 'INTERNAL_ERROR'} - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - updateExtension: (_extensionInput: ExtensionUpdateDraftMutationVariables) => Promise.reject(systemError), // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors -- testing non-Error rejection handling, - }) - - await inTemporaryDirectory(async (tmpDir) => { - const mockExtension = await testUIExtension({ - devUUID: '1', - directory: tmpDir, - type: 'web_pixel_extension', - uid: 'uid1', - }) - - await mkdir(joinPath(tmpDir, 'uid1', 'dist')) - const outputPath = mockExtension.getOutputPathForDirectory(tmpDir) - await writeFile(outputPath, 'test content') - - await updateExtensionDraft({ - extension: mockExtension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration: placeholderAppConfiguration, - bundlePath: tmpDir, - }) - - expect(stderr.write).toHaveBeenCalledWith('Error updating extension draft for test-ui-extension: Unknown error') - }) - }) -}) diff --git a/packages/app/src/cli/services/dev/update-extension.ts b/packages/app/src/cli/services/dev/update-extension.ts deleted file mode 100644 index 7252018f7b5..00000000000 --- a/packages/app/src/cli/services/dev/update-extension.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ExtensionUpdateDraftMutation, - ExtensionUpdateDraftMutationVariables, -} from '../../api/graphql/partners/generated/update-draft.js' -import {AppConfiguration} from '../../models/app/app.js' -import {ExtensionInstance} from '../../models/extensions/extension-instance.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {themeExtensionConfig} from '../deploy/theme-extension-config.js' -import {readFile} from '@shopify/cli-kit/node/fs' -import {outputInfo} from '@shopify/cli-kit/node/output' -import {Writable} from 'stream' - -interface UpdateExtensionDraftOptions { - extension: ExtensionInstance - developerPlatformClient: DeveloperPlatformClient - apiKey: string - registrationId: string - stdout: Writable - stderr: Writable - appConfiguration: AppConfiguration - bundlePath: string -} - -export async function updateExtensionDraft({ - extension, - developerPlatformClient, - apiKey, - registrationId, - stdout, - stderr, - appConfiguration, - bundlePath, -}: UpdateExtensionDraftOptions) { - let encodedFile: string | undefined - const outputPath = extension.getOutputPathForDirectory(bundlePath) - if (extension.features.includes('esbuild')) { - const content = await readFile(outputPath) - if (!content) return - encodedFile = Buffer.from(content).toString('base64') - } - - let config - if (extension.isThemeExtension) { - // When updating just the theme extension draft, upload the files as part of the config. - config = await themeExtensionConfig(extension) - } else { - config = (await extension.deployConfig({apiKey, appConfiguration})) ?? {} - } - - const draftableConfig: {[key: string]: unknown} = { - ...config, - serialized_script: encodedFile, - } - if (extension.isFunctionExtension) { - // For function drafts we need to use the `extension.outputPath` instead of `bundlePath` - // The wasm in the bundle path is encoded in base64. - const compiledFiles = await readFile(extension.outputPath, {encoding: 'base64'}) - draftableConfig.uploaded_files = {'dist/index.wasm': compiledFiles} - } - const extensionInput: ExtensionUpdateDraftMutationVariables = { - apiKey, - config: JSON.stringify(draftableConfig), - handle: extension.handle, - context: extension.contextValue, - registrationId, - } - - let mutationResult: ExtensionUpdateDraftMutation | undefined - let errors: {message: string}[] = [] - try { - mutationResult = await developerPlatformClient.updateExtension(extensionInput) - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (error: unknown) { - errors = [{message: 'Unknown error'}] - - if (error && typeof error === 'object') { - const errorObj = error as {errors?: {message: string}[]; message?: string} - if (errorObj.errors?.length) { - errors = errorObj.errors - } else if (errorObj.message) { - errors = [{message: errorObj.message}] - } - } else if (typeof error === 'string') { - errors = [{message: error}] - } - } - const userErrors = mutationResult?.extensionUpdateDraft?.userErrors - if (userErrors?.length) { - errors.push(...userErrors) - } - if (errors.length > 0) { - const errorMessages = errors.map((error: {message: string}) => error.message).join(', ') - stderr.write(`${extension.draftMessages.errorMessage}: ${errorMessages}`) - } else { - const draftUpdateSuccesMessage = extension.draftMessages.successMessage - if (draftUpdateSuccesMessage) outputInfo(draftUpdateSuccesMessage, stdout) - } -} diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index e8959910772..7ffc52e1d6a 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -43,10 +43,6 @@ import { import {RemoteSpecification} from '../api/graphql/extension_specifications.js' import {MigrateAppModuleSchema, MigrateAppModuleVariables} from '../api/graphql/extension_migrate_app_module.js' import {AppManifest} from '../models/app/app.js' -import { - ExtensionUpdateDraftMutation, - ExtensionUpdateDraftMutationVariables, -} from '../api/graphql/partners/generated/update-draft.js' import {DevSessionCreateMutation} from '../api/graphql/app-dev/generated/dev-session-create.js' import {DevSessionUpdateMutation} from '../api/graphql/app-dev/generated/dev-session-update.js' import {DevSessionDeleteMutation} from '../api/graphql/app-dev/generated/dev-session-delete.js' @@ -270,7 +266,6 @@ export interface DeveloperPlatformClient { appVersionsDiff: (app: MinimalOrganizationApp, version: AppVersionIdentifiers) => Promise generateSignedUploadUrl: (app: MinimalAppIdentifiers) => Promise createExtension: (input: ExtensionCreateVariables) => Promise - updateExtension: (input: ExtensionUpdateDraftMutationVariables) => Promise deploy: (input: AppDeployOptions) => Promise release: (input: {app: MinimalOrganizationApp; version: AppVersionIdentifiers}) => Promise convertToTransferDisabledStore: ( diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 3bb3ee7528c..bb9fc2b2cef 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -75,10 +75,6 @@ import { MigrateToUiExtensionSchema, } from '../../api/graphql/extension_migrate_to_ui_extension.js' import {MigrateAppModuleSchema, MigrateAppModuleVariables} from '../../api/graphql/extension_migrate_app_module.js' -import { - ExtensionUpdateDraftMutation, - ExtensionUpdateDraftMutationVariables, -} from '../../api/graphql/partners/generated/update-draft.js' import {AppHomeSpecIdentifier} from '../../models/extensions/specifications/app_config_app_home.js' import {BrandingSpecIdentifier} from '../../models/extensions/specifications/app_config_branding.js' import {AppAccessSpecIdentifier} from '../../models/extensions/specifications/app_config_app_access.js' @@ -748,10 +744,6 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async updateExtension(_extensionInput: ExtensionUpdateDraftMutationVariables): Promise { - throw new BugError('Not implemented: updateExtension') - } - async deploy({ appManifest, appId, diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index b80967269a5..bc48f918dbb 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -133,11 +133,6 @@ import { import {AppLogsSubscribeMutation, AppLogsSubscribeResponse} from '../../api/graphql/subscribe_to_app_logs.js' import {AllOrgs} from '../../api/graphql/partners/generated/all-orgs.js' -import { - ExtensionUpdateDraft, - ExtensionUpdateDraftMutation, - ExtensionUpdateDraftMutationVariables, -} from '../../api/graphql/partners/generated/update-draft.js' import {FindAppQuery, FindAppQuerySchema, FindAppQueryVariables} from '../../api/graphql/find_app.js' import { FindOrganizationQuery, @@ -466,10 +461,6 @@ export class PartnersClient implements DeveloperPlatformClient { return this.request(ExtensionCreateQuery, input) } - async updateExtension(extensionInput: ExtensionUpdateDraftMutationVariables): Promise { - return this.requestDoc(ExtensionUpdateDraft, extensionInput) - } - async deploy(deployInput: AppDeployOptions): Promise { const {organizationId, ...deployOptions} = deployInput // Enforce the type From 427620ebbac1b1f520ed13c58249e4c0e45c87dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 7 Jul 2026 15:11:11 +0200 Subject: [PATCH 2/3] Stabilize config link e2e prompt input Assisted-By: devx/df67c0ff-0bc3-434a-b5d7-091c8efbac1e --- packages/e2e/setup/app.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/e2e/setup/app.ts b/packages/e2e/setup/app.ts index dbdd212cae3..975cc65eadb 100644 --- a/packages/e2e/setup/app.ts +++ b/packages/e2e/setup/app.ts @@ -284,6 +284,12 @@ export async function configLink( // Without this, an Enter press arrives mid-mount and a subsequent render can // flip the prompt state unexpectedly (e.g. turning a select into search mode). const settle = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms)) + const typeText = async (text: string) => { + for (const char of text) { + proc.ptyProcess.write(char) + await settle(10) + } + } try { // With --organization-id, the first prompt is either "Create this project" @@ -309,7 +315,8 @@ export async function configLink( // text, wait for it to be consumed, then write \r separately. await proc.waitForOutput('App name', CLI_TIMEOUT.medium) await settle() - proc.ptyProcess.write(ctx.appName) + await typeText(ctx.appName) + await proc.waitForOutput(ctx.appName, CLI_TIMEOUT.medium) await settle() proc.sendKey('\r') From e084cf5d3a49b03355dfe2047618f5870821a6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Tue, 7 Jul 2026 15:31:17 +0200 Subject: [PATCH 3/3] Bound e2e app deletion lookup time Assisted-By: devx/df67c0ff-0bc3-434a-b5d7-091c8efbac1e --- packages/e2e/setup/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e/setup/app.ts b/packages/e2e/setup/app.ts index 975cc65eadb..d8b5f1d349a 100644 --- a/packages/e2e/setup/app.ts +++ b/packages/e2e/setup/app.ts @@ -385,11 +385,11 @@ export async function deleteAppFromDevDashboard(page: Page, appUrl: string): Pro // Button can be below the fold, and takes ~1-2s to enable after uninstall (one reload covers propagation lag). // If it stays disabled after reload, installs remain — fail fast for caller. const deleteBtn = page.locator('button:has-text("Delete app")').first() - await deleteBtn.scrollIntoViewIfNeeded() + await deleteBtn.scrollIntoViewIfNeeded({timeout: BROWSER_TIMEOUT.long}) if (!(await deleteBtn.isEnabled())) { await page.reload({waitUntil: 'domcontentloaded'}) await page.waitForTimeout(BROWSER_TIMEOUT.medium) - await deleteBtn.scrollIntoViewIfNeeded() + await deleteBtn.scrollIntoViewIfNeeded({timeout: BROWSER_TIMEOUT.long}) if (!(await deleteBtn.isEnabled())) throw new Error('STILL_HAS_INSTALLS') } await deleteBtn.click({timeout: 2 * BROWSER_TIMEOUT.long})