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
6 changes: 2 additions & 4 deletions packages/app/src/cli/models/app/identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ describe('getAppIdentifiers', () => {
})

// Then
expect(got.app).toEqual('FOO')
expect((got.extensions ?? {})['test-ui-extension']).toEqual('BAR')
expect(got['test-ui-extension']).toEqual('BAR')
})
})

Expand All @@ -229,8 +228,7 @@ describe('getAppIdentifiers', () => {
)

// Then
expect(got.app).toEqual('FOO')
expect((got.extensions ?? {})['test-ui-extension']).toEqual('BAR')
expect(got['test-ui-extension']).toEqual('BAR')
})
})
})
7 changes: 2 additions & 5 deletions packages/app/src/cli/models/app/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ interface GetAppIdentifiersOptions {
export function getAppIdentifiers(
{app}: GetAppIdentifiersOptions,
systemEnvironment = process.env,
): Partial<UuidOnlyIdentifiers> {
): ExtensionUuidsByLocalIdentifier {
const envVariables = {
...app.dotenv?.variables,
...(systemEnvironment as {[variable: string]: string}),
Expand All @@ -113,8 +113,5 @@ export function getAppIdentifiers(
}
app.allExtensions.forEach(processExtension)

return {
app: envVariables[app.idEnvironmentVariableName],
extensions: extensionsIdentifiers,
}
return extensionsIdentifiers
}
50 changes: 12 additions & 38 deletions packages/app/src/cli/services/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {fetchOrganizations, fetchOrgFromId} from './dev/fetch.js'
import {selectOrCreateApp} from './dev/select-app.js'
import {selectStore} from './dev/select-store.js'
import {ensureDeploymentIdsPresence} from './context/identifiers.js'
import {ensureDeployIdentifiersFromAppVersion} from './context/deploy-app-version.js'
import {appFromIdentifiers, ensureDeployContext} from './context.js'
import {CachedAppInfo} from './local-storage.js'
import link from './app/config/link.js'
Expand Down Expand Up @@ -110,7 +110,7 @@ vi.mock('./dev/select-store')
vi.mock('../prompts/dev')
vi.mock('@shopify/organizations')
vi.mock('../models/app/identifiers')
vi.mock('./context/identifiers')
vi.mock('./context/deploy-app-version')
vi.mock('../models/app/loader.js')
vi.mock('@shopify/cli-kit/node/node-package-manager.js')
vi.mock('@shopify/cli-kit/node/ui')
Expand All @@ -134,7 +134,7 @@ beforeAll(async () => {
})

beforeEach(async () => {
vi.mocked(getAppIdentifiers).mockReturnValue({app: undefined})
vi.mocked(getAppIdentifiers).mockReturnValue({})
vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1)
vi.mocked(selectOrCreateApp).mockResolvedValue(APP1)
vi.mocked(selectStore).mockResolvedValue(STORE1)
Expand Down Expand Up @@ -174,13 +174,7 @@ describe('ensureDeployContext', () => {
test('does not abort when force is true and include_config_on_deploy is not set', async () => {
// Given
const app = testAppWithConfig({config: {client_id: APP2.apiKey}})
const identifiers = {
app: APP2.apiKey,
extensions: {},
extensionIds: {},
extensionsNonUuidManaged: {},
}
vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers)
vi.mocked(ensureDeployIdentifiersFromAppVersion).mockResolvedValue(emptyDeployIdentifiers())
vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml')

const options = {
Expand All @@ -196,13 +190,7 @@ describe('ensureDeployContext', () => {
test('removes the include_config_on_deploy field when using app management API and the value is true', async () => {
// Given
const app = testAppWithConfig({config: {client_id: APP2.apiKey, build: {include_config_on_deploy: true}}})
const identifiers = {
app: APP2.apiKey,
extensions: {},
extensionIds: {},
extensionsNonUuidManaged: {},
}
vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers)
vi.mocked(ensureDeployIdentifiersFromAppVersion).mockResolvedValue(emptyDeployIdentifiers())
vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml')
mockTomlFileRemove.mockResolvedValue(undefined)

Expand Down Expand Up @@ -238,13 +226,7 @@ describe('ensureDeployContext', () => {
test('removes the include_config_on_deploy field when using app management API and the value is false', async () => {
// Given
const app = testAppWithConfig({config: {client_id: APP2.apiKey, build: {include_config_on_deploy: false}}})
const identifiers = {
app: APP2.apiKey,
extensions: {},
extensionIds: {},
extensionsNonUuidManaged: {},
}
vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers)
vi.mocked(ensureDeployIdentifiersFromAppVersion).mockResolvedValue(emptyDeployIdentifiers())
vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml')
mockTomlFileRemove.mockResolvedValue(undefined)

Expand Down Expand Up @@ -280,13 +262,7 @@ describe('ensureDeployContext', () => {
test('sets didMigrateExtensionsToDevDash to true when app modules are missing registration IDs', async () => {
// Given
const app = testAppWithConfig({config: {client_id: APP2.apiKey}})
const identifiers = {
app: APP2.apiKey,
extensions: {},
extensionIds: {},
extensionsNonUuidManaged: {},
}
vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers)
vi.mocked(ensureDeployIdentifiersFromAppVersion).mockResolvedValue(emptyDeployIdentifiers())
vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml')

const activeAppVersion = {
Expand Down Expand Up @@ -325,13 +301,7 @@ describe('ensureDeployContext', () => {
test('sets didMigrateExtensionsToDevDash to false when all app modules have registration IDs', async () => {
// Given
const app = testAppWithConfig({config: {client_id: APP2.apiKey}})
const identifiers = {
app: APP2.apiKey,
extensions: {},
extensionIds: {},
extensionsNonUuidManaged: {},
}
vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers)
vi.mocked(ensureDeployIdentifiersFromAppVersion).mockResolvedValue(emptyDeployIdentifiers())
vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml')

const activeAppVersion = {
Expand Down Expand Up @@ -422,6 +392,10 @@ describe('appFromIdentifiers', () => {
})
})

function emptyDeployIdentifiers() {
return {appModuleUuids: {}, appModuleRegistrationIds: {}}
}

const renderTryMessage = (isOrg: boolean, identifier: string) => [
{
list: {
Expand Down
12 changes: 5 additions & 7 deletions packages/app/src/cli/services/context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {selectOrCreateApp} from './dev/select-app.js'
import {fetchOrganizations, fetchOrgFromId} from './dev/fetch.js'
import {ensureDeploymentIdsPresence} from './context/identifiers.js'
import {ensureDeployIdentifiersFromAppVersion} from './context/deploy-app-version.js'
import {CachedAppInfo} from './local-storage.js'
import {DeployOptions} from './deploy.js'
import {formatConfigInfoBody} from './format-config-info-body.js'
import {AppInterface, AppLinkedInterface} from '../models/app/app.js'
import {Identifiers, updateAppIdentifiers, getAppIdentifiers} from '../models/app/identifiers.js'
import {DeployIdentifiers, getAppIdentifiers} from '../models/app/identifiers.js'
import {Organization, OrganizationApp, OrganizationSource, OrganizationStore} from '../models/organization.js'
import metadata from '../metadata.js'
import {getAppConfigurationFileName} from '../models/app/loader.js'
Expand Down Expand Up @@ -101,7 +101,7 @@ export const appFromIdentifiers = async (options: AppFromIdOptions): Promise<Org
}

interface EnsureDeployContextResult {
identifiers: Identifiers
deployIdentifiers: DeployIdentifiers
didMigrateExtensionsToDevDash: boolean
}

Expand Down Expand Up @@ -131,7 +131,7 @@ export async function ensureDeployContext(options: DeployOptions): Promise<Ensur
messages: [resetHelpMessage],
})

const identifiers = await ensureDeploymentIdsPresence({
const deployIdentifiers = await ensureDeployIdentifiersFromAppVersion({
app,
appId: remoteApp.apiKey,
appName: remoteApp.title,
Expand All @@ -144,15 +144,13 @@ export async function ensureDeployContext(options: DeployOptions): Promise<Ensur
allowDeletes: options.allowDeletes,
})

await updateAppIdentifiers({app, identifiers, command: 'deploy'})

// if the current active app version is missing user_identifiers in some app module, then we are migrating to dev dash
let didMigrateExtensionsToDevDash = false
if (activeAppVersion) {
didMigrateExtensionsToDevDash = activeAppVersion.appModuleVersions.some((version) => !version.registrationId)
}

return {identifiers, didMigrateExtensionsToDevDash}
return {deployIdentifiers, didMigrateExtensionsToDevDash}
}

async function removeIncludeConfigOnDeployField(localApp: AppInterface) {
Expand Down
57 changes: 56 additions & 1 deletion packages/app/src/cli/services/context/breakdown-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {ExtensionSpecification, isAppConfigSpecification} from '../../models/ext
import {rewriteConfiguration} from '../app/write-app-configuration-file.js'
import {AppConfigurationUsedByCli} from '../../models/extensions/specifications/types/app_config.js'
import {removeTrailingSlash} from '../../models/extensions/specifications/validation/common.js'
import {deepCompare, deepDifference} from '@shopify/cli-kit/common/object'
import {deepCompare, deepCompareWithOrderInsensitiveArrays, deepDifference} from '@shopify/cli-kit/common/object'
import {zod} from '@shopify/cli-kit/node/schema'

export interface ConfigExtensionIdentifiersBreakdown {
Expand Down Expand Up @@ -419,3 +419,58 @@ function loadDashboardIdentifiersBreakdown(currentRegistrations: RemoteSource[],
unchanged: toUpdate,
}
}

export function configExtensionsIdentifiersReleaseBreakdown({
localApp,
versionAppModules,
activeAppVersion,
}: {
localApp: AppInterface
versionAppModules: AppModuleVersion[]
activeAppVersion?: AppVersion
}) {
if (localApp.allExtensions.filter((extension) => extension.isAppConfigExtension).length === 0) return
const versionConfig = remoteAppConfigurationExtensionContent(
versionAppModules,
localApp.specifications ?? [],
localApp.remoteFlags,
)
const activeConfig = remoteAppConfigurationExtensionContent(
activeAppVersion?.appModuleVersions ?? [],
localApp.specifications ?? [],
localApp.remoteFlags,
)
return buildConfigExtensionIdentifiersBreakdown(versionConfig, activeConfig)
}

export function buildConfigExtensionIdentifiersBreakdown(
localConfig: {[key: string]: unknown},
remoteConfig: {[key: string]: unknown},
): ConfigExtensionIdentifiersBreakdown | undefined {
const fieldNames = new Set([...Object.keys(localConfig), ...Object.keys(remoteConfig)])
if (fieldNames.size === 0) return undefined

const breakdown: ConfigExtensionIdentifiersBreakdown = {
existingFieldNames: [],
existingUpdatedFieldNames: [],
newFieldNames: [],
deletedFieldNames: [],
}

for (const fieldName of fieldNames) {
const localValue = localConfig[fieldName]
const remoteValue = remoteConfig[fieldName]

if (localValue === undefined) {
breakdown.deletedFieldNames.push(fieldName)
} else if (remoteValue === undefined) {
breakdown.newFieldNames.push(fieldName)
} else if (deepCompareWithOrderInsensitiveArrays(localValue, remoteValue)) {
breakdown.existingFieldNames.push(fieldName)
} else {
breakdown.existingUpdatedFieldNames.push(fieldName)
}
}

return breakdown
}
100 changes: 100 additions & 0 deletions packages/app/src/cli/services/context/deploy-app-version-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {EnsureDeploymentIdsPresenceOptions, RemoteSource} from './identifiers.js'
import {extensionMigrationPrompt} from './prompts.js'
import {migrateFlowExtensions} from '../dev/migrate-flow-extension.js'
import {migrateExtensionsToUIExtension} from '../dev/migrate-to-ui-extension.js'
import {
AdminLinkModulesMap,
FlowModulesMap,
getModulesToMigrate,
MarketingModulesMap,
migrateAppModules,
UIModulesMap,
} from '../dev/migrate-app-module.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {PartnersClient} from '../../utilities/developer-platform-client/partners-client.js'
import {AbortSilentError} from '@shopify/cli-kit/node/error'

/** Returns a fresh active app version when migrations changed remote modules. */
export async function activeAppVersionAfterMigrations(options: EnsureDeploymentIdsPresenceOptions) {
const didMigrateExtensions = await migrateExtensionsIfNeeded(options)
return didMigrateExtensions
? options.developerPlatformClient.activeAppVersion(options.remoteApp)
: options.activeAppVersion
}

/** Runs supported legacy extension migrations before deploy classification. */
async function migrateExtensionsIfNeeded(options: EnsureDeploymentIdsPresenceOptions) {
const {app, appId, developerPlatformClient, envIdentifiers, remoteApp} = options
const registrations = await developerPlatformClient.appExtensionRegistrations(remoteApp, options.activeAppVersion)
const localExtensions = app.allExtensions
const identifiers = envIdentifiers
const remoteExtensions = registrations.app.extensionRegistrations
const dashboardExtensions = registrations.app.dashboardManagedExtensionRegistrations
const migrationClient = PartnersClient.getInstance()

let didMigrate = false
let migratedRemoteExtensions = remoteExtensions

const uiExtensionsToMigrate = getModulesToMigrate(
localExtensions,
migratedRemoteExtensions,
identifiers,
UIModulesMap,
)
if (uiExtensionsToMigrate.length > 0) {
const confirmedMigration = await extensionMigrationPrompt(uiExtensionsToMigrate)
if (!confirmedMigration) throw new AbortSilentError()
migratedRemoteExtensions = await migrateExtensionsToUIExtension({
extensionsToMigrate: uiExtensionsToMigrate,
appId,
remoteExtensions: migratedRemoteExtensions,
migrationClient,
})
didMigrate = true
}

const dashboardMigrations = [
{typesMap: FlowModulesMap, migrate: migrateFlowExtensions},
migrationGroup(MarketingModulesMap, 'marketing_activity'),
migrationGroup(AdminLinkModulesMap, 'admin_link'),
]

for (const migration of dashboardMigrations) {
const extensionsToMigrate = getModulesToMigrate(
localExtensions,
dashboardExtensions,
identifiers,
migration.typesMap,
)
if (extensionsToMigrate.length === 0) continue

// eslint-disable-next-line no-await-in-loop
const confirmedMigration = await extensionMigrationPrompt(extensionsToMigrate, false)
if (!confirmedMigration) throw new AbortSilentError()

// eslint-disable-next-line no-await-in-loop
const newRemoteExtensions = await migration.migrate({
extensionsToMigrate,
appId,
remoteExtensions: dashboardExtensions,
migrationClient,
})
migratedRemoteExtensions = migratedRemoteExtensions.concat(newRemoteExtensions)
didMigrate = true
}

return didMigrate
}

/** Adapts app-module migrations to the shared dashboard migration loop. */
function migrationGroup(typesMap: {[key: string]: string[]}, type: string) {
return {
typesMap,
migrate: (options: {
extensionsToMigrate: Parameters<typeof migrateAppModules>[0]['extensionsToMigrate']
appId: string
remoteExtensions: RemoteSource[]
migrationClient: DeveloperPlatformClient
}) => migrateAppModules({...options, type}),
}
}
Loading
Loading