Skip to content
Closed
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
3 changes: 2 additions & 1 deletion packages/app/src/cli/commands/app/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}
Expand Down
42 changes: 26 additions & 16 deletions packages/app/src/cli/commands/app/import-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -24,21 +25,31 @@ export default class ImportExtensions extends AppLinkedCommand {

async run(): Promise<AppLinkedCommandOutput> {
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.']})
Expand All @@ -47,8 +58,7 @@ export default class ImportExtensions extends AppLinkedCommand {
await importExtensions({
...appContext,
extensions,
extensionTypes: migrationChoice.extensionTypes,
buildTomlObject: migrationChoice.buildTomlObject,
migrationChoice,
})
}

Expand Down
36 changes: 32 additions & 4 deletions packages/app/src/cli/prompts/import-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: [
Expand All @@ -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<MigrationChoice> {
if (migrationChoices.length === 1 && migrationChoices[0]) {
if (migrationChoices.length === 1 && migrationChoices[0] && !migrationChoices[0].neverSelectAutomatically) {
return migrationChoices[0]
}

Expand Down
18 changes: 15 additions & 3 deletions packages/app/src/cli/services/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand All @@ -64,14 +68,16 @@ interface ImportExtensionsIfNeededOptions {
remoteApp: OrganizationApp
developerPlatformClient: DeveloperPlatformClient
force: boolean
organization: Organization
specifications: RemoteAwareExtensionSpecification[]
}

async function handleSupportedDashboardExtensions(
options: ImportExtensionsIfNeededOptions & {
extensions: ExtensionRegistration[]
},
): Promise<AppLinkedInterface> {
const {app, remoteApp, developerPlatformClient, force, extensions} = options
const {app, remoteApp, developerPlatformClient, force, extensions, organization, specifications} = options

if (force || !isTTY()) {
return app
Expand All @@ -96,6 +102,8 @@ async function handleSupportedDashboardExtensions(
remoteApp,
developerPlatformClient,
extensions,
organization,
specifications,
})
return reloadApp(app)
}
Expand All @@ -108,7 +116,7 @@ async function handleUnsupportedDashboardExtensions(
extensions: ExtensionRegistration[]
},
): Promise<AppLinkedInterface> {
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`,
Expand All @@ -133,6 +141,8 @@ async function handleUnsupportedDashboardExtensions(
remoteApp,
developerPlatformClient,
extensions,
organization,
specifications,
})
return reloadApp(app)
} else {
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
62 changes: 42 additions & 20 deletions packages/app/src/cli/services/import-extensions.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
})
}),
)
}

Expand Down
Loading