diff --git a/packages/app/src/cli/commands/app/deploy.ts b/packages/app/src/cli/commands/app/deploy.ts index 1ea49402070..f5cdbd21653 100644 --- a/packages/app/src/cli/commands/app/deploy.ts +++ b/packages/app/src/cli/commands/app/deploy.ts @@ -87,7 +87,7 @@ export default class Deploy extends AppLinkedCommand { } this.failMissingNonTTYFlags(flags, requiredNonTTYFlags) - const {app, remoteApp, developerPlatformClient, organization} = await linkedAppContext({ + const {app, remoteApp, developerPlatformClient, organization, specifications} = await linkedAppContext({ directory: flags.path, clientId, forceRelink: flags.reset, @@ -106,6 +106,7 @@ export default class Deploy extends AppLinkedCommand { version: flags.version, commitReference: flags['source-control-url'], skipBuild: flags['no-build'], + specifications, }) return {app: result.app} diff --git a/packages/app/src/cli/commands/app/import-extensions.ts b/packages/app/src/cli/commands/app/import-extensions.ts index bd7d77bf4b6..93764cb06f3 100644 --- a/packages/app/src/cli/commands/app/import-extensions.ts +++ b/packages/app/src/cli/commands/app/import-extensions.ts @@ -6,7 +6,8 @@ import {getMigrationChoices, selectMigrationChoice} from '../../prompts/import-e import {getExtensions} from '../../services/fetch-extensions.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' -import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {renderSingleTask, renderSuccess} from '@shopify/cli-kit/node/ui' +import {outputContent} from '@shopify/cli-kit/node/output' export default class ImportExtensions extends AppLinkedCommand { static description = 'Import dashboard-managed extensions into your app.' @@ -24,21 +25,31 @@ export default class ImportExtensions extends AppLinkedCommand { async run(): Promise { const {flags} = await this.parse(ImportExtensions) - const appContext = await linkedAppContext({ - directory: flags.path, - clientId: flags['client-id'], - forceRelink: flags.reset, - userProvidedConfigName: flags.config, - }) + const {appContext, extensions, migrationChoices} = await renderSingleTask({ + title: outputContent`Loading app`, + task: async () => { + const appContext = await linkedAppContext({ + directory: flags.path, + clientId: flags['client-id'], + forceRelink: flags.reset, + userProvidedConfigName: flags.config, + }) - const extensions = await getExtensions({ - developerPlatformClient: appContext.developerPlatformClient, - apiKey: appContext.remoteApp.apiKey, - organizationId: appContext.remoteApp.organizationId, - extensionTypes: allExtensionTypes, - }) + const extensions = await getExtensions({ + developerPlatformClient: appContext.developerPlatformClient, + apiKey: appContext.remoteApp.apiKey, + organizationId: appContext.remoteApp.organizationId, + extensionTypes: allExtensionTypes, + }) - const migrationChoices = getMigrationChoices(extensions) + const migrationChoices = getMigrationChoices(extensions) + return { + appContext, + extensions, + migrationChoices, + } + }, + }) if (migrationChoices.length === 0) { renderSuccess({headline: ['No extensions to migrate.']}) @@ -47,8 +58,7 @@ export default class ImportExtensions extends AppLinkedCommand { await importExtensions({ ...appContext, extensions, - extensionTypes: migrationChoice.extensionTypes, - buildTomlObject: migrationChoice.buildTomlObject, + migrationChoice, }) } diff --git a/packages/app/src/cli/prompts/import-extensions.ts b/packages/app/src/cli/prompts/import-extensions.ts index 051f4be1f1d..32de41053bb 100644 --- a/packages/app/src/cli/prompts/import-extensions.ts +++ b/packages/app/src/cli/prompts/import-extensions.ts @@ -8,9 +8,14 @@ import {CurrentAppConfiguration} from '../models/app/app.js' import {AbortError} from '@shopify/cli-kit/node/error' import {renderSelectPrompt} from '@shopify/cli-kit/node/ui' -export interface MigrationChoice { +interface MigrationChoiceCommon { label: string value: string + neverSelectAutomatically?: boolean +} + +type ExtensionMigrationChoice = MigrationChoiceCommon & { + mode: 'extension' extensionTypes: string[] buildTomlObject: ( ext: ExtensionRegistration, @@ -19,8 +24,19 @@ export interface MigrationChoice { ) => string } +type SupportedShopImportSources = 'declarative definitions' + +export type ShopImportMigrationChoice = MigrationChoiceCommon & { + mode: 'shop-import' + // Only declarative definitions are supported for shop import at present. + value: SupportedShopImportSources +} + +export type MigrationChoice = ExtensionMigrationChoice | ShopImportMigrationChoice + export const allMigrationChoices: MigrationChoice[] = [ { + mode: 'extension', label: 'Payments Extensions', value: 'payments', extensionTypes: [ @@ -34,39 +50,51 @@ export const allMigrationChoices: MigrationChoice[] = [ buildTomlObject: buildPaymentsTomlObject, }, { + mode: 'extension', label: 'Flow Extensions', value: 'flow', extensionTypes: ['flow_action_definition', 'flow_trigger_definition', 'flow_trigger_discovery_webhook'], buildTomlObject: buildFlowTomlObject, }, { + mode: 'extension', label: 'Marketing Activity Extensions', value: 'marketing activity', extensionTypes: ['marketing_activity_extension'], buildTomlObject: buildMarketingActivityTomlObject, }, { + mode: 'extension', label: 'Subscription Link Extensions', value: 'subscription link', extensionTypes: ['subscription_link', 'subscription_link_extension'], buildTomlObject: buildSubscriptionLinkTomlObject, }, { + mode: 'extension', label: 'Admin Link extensions', value: 'link extension', extensionTypes: ['app_link', 'bulk_action'], buildTomlObject: buildAdminLinkTomlObject, }, + { + mode: 'shop-import', + label: 'Metafields and Metaobject definitions', + value: 'declarative definitions', + neverSelectAutomatically: true, + }, ] export function getMigrationChoices(extensions: ExtensionRegistration[]): MigrationChoice[] { - return allMigrationChoices.filter((choice) => - choice.extensionTypes.some((type) => extensions.some((ext) => ext.type.toLowerCase() === type.toLowerCase())), + return allMigrationChoices.filter( + (choice) => + choice.mode === 'shop-import' || + choice.extensionTypes.some((type) => extensions.some((ext) => ext.type.toLowerCase() === type.toLowerCase())), ) } export async function selectMigrationChoice(migrationChoices: MigrationChoice[]): Promise { - if (migrationChoices.length === 1 && migrationChoices[0]) { + if (migrationChoices.length === 1 && migrationChoices[0] && !migrationChoices[0].neverSelectAutomatically) { return migrationChoices[0] } diff --git a/packages/app/src/cli/services/deploy.ts b/packages/app/src/cli/services/deploy.ts index 00b89f0807a..060fbf80df9 100644 --- a/packages/app/src/cli/services/deploy.ts +++ b/packages/app/src/cli/services/deploy.ts @@ -11,6 +11,7 @@ import {Organization, OrganizationApp} from '../models/organization.js' import {reloadApp} from '../models/app/loader.js' import {ExtensionRegistration} from '../api/graphql/all_app_extension_registrations.js' import {getTomls} from '../utilities/app/config/getTomls.js' +import {RemoteAwareExtensionSpecification} from '../models/extensions/specification.js' import {renderInfo, renderSuccess, renderTasks, renderConfirmationPrompt, isTTY} from '@shopify/cli-kit/node/ui' import {mkdir} from '@shopify/cli-kit/node/fs' import {joinPath, dirname} from '@shopify/cli-kit/node/path' @@ -52,6 +53,9 @@ export interface DeployOptions { /** If true, skip building any elements of the app that require building */ skipBuild: boolean + + /** The specifications of the extensions */ + specifications: RemoteAwareExtensionSpecification[] } interface TasksContext { @@ -64,6 +68,8 @@ interface ImportExtensionsIfNeededOptions { remoteApp: OrganizationApp developerPlatformClient: DeveloperPlatformClient force: boolean + organization: Organization + specifications: RemoteAwareExtensionSpecification[] } async function handleSupportedDashboardExtensions( @@ -71,7 +77,7 @@ async function handleSupportedDashboardExtensions( extensions: ExtensionRegistration[] }, ): Promise { - const {app, remoteApp, developerPlatformClient, force, extensions} = options + const {app, remoteApp, developerPlatformClient, force, extensions, organization, specifications} = options if (force || !isTTY()) { return app @@ -96,6 +102,8 @@ async function handleSupportedDashboardExtensions( remoteApp, developerPlatformClient, extensions, + organization, + specifications, }) return reloadApp(app) } @@ -108,7 +116,7 @@ async function handleUnsupportedDashboardExtensions( extensions: ExtensionRegistration[] }, ): Promise { - const {app, remoteApp, developerPlatformClient, force, extensions} = options + const {app, remoteApp, developerPlatformClient, force, extensions, organization, specifications} = options const message = [ `App can't be deployed until Partner Dashboard managed extensions are added to your version or removed from your app:\n`, @@ -133,6 +141,8 @@ async function handleUnsupportedDashboardExtensions( remoteApp, developerPlatformClient, extensions, + organization, + specifications, }) return reloadApp(app) } else { @@ -171,13 +181,15 @@ export async function importExtensionsIfNeeded(options: ImportExtensionsIfNeeded } export async function deploy(options: DeployOptions) { - const {remoteApp, developerPlatformClient, noRelease, force} = options + const {remoteApp, developerPlatformClient, noRelease, force, organization, specifications} = options const app = await importExtensionsIfNeeded({ app: options.app, remoteApp, developerPlatformClient, force, + organization, + specifications, }) const {identifiers, didMigrateExtensionsToDevDash} = await ensureDeployContext({ diff --git a/packages/app/src/cli/services/generate/shop-import/declarative-definitions.ts b/packages/app/src/cli/services/generate/shop-import/declarative-definitions.ts index 69f7550330b..60b91dc77a7 100644 --- a/packages/app/src/cli/services/generate/shop-import/declarative-definitions.ts +++ b/packages/app/src/cli/services/generate/shop-import/declarative-definitions.ts @@ -200,7 +200,7 @@ export function processDeclarativeDefinitionNodes( } } -async function _importDeclarativeDefinitions(options: ImportDeclarativeDefinitionsOptions) { +export async function importDeclarativeDefinitions(options: ImportDeclarativeDefinitionsOptions) { const adminSession = await renderSingleTask({ title: outputContent`Connecting to shop`, task: async () => { diff --git a/packages/app/src/cli/services/import-extensions.ts b/packages/app/src/cli/services/import-extensions.ts index 2d736b0b0c6..06eb39fb7ff 100644 --- a/packages/app/src/cli/services/import-extensions.ts +++ b/packages/app/src/cli/services/import-extensions.ts @@ -1,34 +1,40 @@ -import {AppLinkedInterface, CurrentAppConfiguration} from '../models/app/app.js' +import {importDeclarativeDefinitions} from './generate/shop-import/declarative-definitions.js' +import {AppLinkedInterface} from '../models/app/app.js' import {updateAppIdentifiers, IdentifiersExtensions} from '../models/app/identifiers.js' import {ExtensionRegistration} from '../api/graphql/all_app_extension_registrations.js' import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' import {MAX_EXTENSION_HANDLE_LENGTH} from '../models/extensions/schemas.js' -import {OrganizationApp} from '../models/organization.js' -import {allMigrationChoices, getMigrationChoices} from '../prompts/import-extensions.js' +import {Organization, OrganizationApp} from '../models/organization.js' +import { + allMigrationChoices, + getMigrationChoices, + MigrationChoice, + ShopImportMigrationChoice, +} from '../prompts/import-extensions.js' import {configurationFileNames, blocks} from '../constants.js' +import {RemoteAwareExtensionSpecification} from '../models/extensions/specification.js' import {renderSelectPrompt, renderSuccess} from '@shopify/cli-kit/node/ui' import {basename, joinPath} from '@shopify/cli-kit/node/path' import {removeFile, writeFile, fileExists, mkdir, touchFile} from '@shopify/cli-kit/node/fs' import {outputContent} from '@shopify/cli-kit/node/output' import {slugify, hyphenate} from '@shopify/cli-kit/common/string' -import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' +import {AbortError, AbortSilentError, BugError} from '@shopify/cli-kit/node/error' -export const allExtensionTypes = allMigrationChoices.flatMap((choice) => choice.extensionTypes) +export const allExtensionTypes = allMigrationChoices.flatMap((choice) => + choice.mode === 'extension' ? choice.extensionTypes : [], +) interface ImportAllOptions { app: AppLinkedInterface remoteApp: OrganizationApp developerPlatformClient: DeveloperPlatformClient extensions: ExtensionRegistration[] + organization: Organization + specifications: RemoteAwareExtensionSpecification[] } interface ImportOptions extends ImportAllOptions { - extensionTypes: string[] - buildTomlObject: ( - ext: ExtensionRegistration, - allExtensions: ExtensionRegistration[], - appConfig: CurrentAppConfiguration, - ) => string + migrationChoice: MigrationChoice all?: boolean } @@ -76,8 +82,23 @@ async function handleExtensionDirectory({ return {directory: extensionDirectory, action: DirectoryAction.Write} } +async function importConfigurationFromShop(options: ImportOptions & {migrationChoice: ShopImportMigrationChoice}) { + switch (options.migrationChoice.value) { + case 'declarative definitions': + return importDeclarativeDefinitions(options) + default: + throw new BugError(`Unsupported shop import source: ${options.migrationChoice.value}`) + } +} + export async function importExtensions(options: ImportOptions) { - const {app, remoteApp, developerPlatformClient, extensionTypes, extensions, buildTomlObject, all} = options + const {app, remoteApp, developerPlatformClient, migrationChoice, extensions, all} = options + + if (migrationChoice.mode === 'shop-import') { + return importConfigurationFromShop({...options, migrationChoice}) + } + + const {extensionTypes, buildTomlObject} = migrationChoice let extensionsToMigrate = extensions.filter((ext) => extensionTypes.includes(ext.type.toLowerCase())) extensionsToMigrate = filterOutImportedExtensions(app, extensionsToMigrate) @@ -144,14 +165,15 @@ export function filterOutImportedExtensions(app: AppLinkedInterface, extensions: export async function importAllExtensions(options: ImportAllOptions) { const migrationChoices = getMigrationChoices(options.extensions) await Promise.all( - migrationChoices.map(async (choice) => { - return importExtensions({ - ...options, - extensionTypes: choice.extensionTypes, - buildTomlObject: choice.buildTomlObject, - all: true, - }) - }), + migrationChoices + .filter((choice) => choice.mode === 'extension') + .map(async (choice) => { + return importExtensions({ + ...options, + migrationChoice: choice, + all: true, + }) + }), ) }