diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index d9e9256d642..247eacf44da 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -26,9 +26,14 @@ export type ExtensionFeature = export type TransformationConfig = Record +export interface TransformRemoteToLocalOptions { + readonly flags?: Flag[] + readonly module?: {readonly handle: string} +} + export interface CustomTransformationConfig { forward?: (obj: object, appConfiguration: AppConfiguration, options?: {flags?: Flag[]}) => object - reverse?: (obj: object, options?: {flags?: Flag[]}) => object + reverse?: (obj: object, options?: TransformRemoteToLocalOptions) => object } type ExtensionExperience = 'extension' | 'configuration' @@ -119,7 +124,7 @@ export interface ExtensionSpecification object + transformRemoteToLocal?: (remoteContent: object, options?: TransformRemoteToLocalOptions) => object uidStrategy: UidStrategy diff --git a/packages/app/src/cli/services/app/select-app.ts b/packages/app/src/cli/services/app/select-app.ts index 915e88933b3..1213f0a2326 100644 --- a/packages/app/src/cli/services/app/select-app.ts +++ b/packages/app/src/cli/services/app/select-app.ts @@ -68,7 +68,10 @@ export function remoteAppConfigurationExtensionContent( const config = module.config if (!config) return - remoteAppConfig = deepMergeObjects(remoteAppConfig, configSpec.transformRemoteToLocal?.(config, {flags}) ?? config) + remoteAppConfig = deepMergeObjects( + remoteAppConfig, + configSpec.transformRemoteToLocal?.(config, {flags, module: {handle: module.registrationTitle}}) ?? config, + ) }) return {...remoteAppConfig} diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts index 1b24ebfc74b..8227d245085 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts @@ -13,7 +13,7 @@ import { } from '../../models/app/app.test-data.js' import {OrganizationApp} from '../../models/organization.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' -import {BaseConfigType} from '../../models/extensions/schemas.js' +import {BaseConfigType, BaseSchemaWithoutHandle} from '../../models/extensions/schemas.js' import {createConfigExtensionSpecification} from '../../models/extensions/specification.js' import {AppModuleVersion, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js' @@ -623,6 +623,63 @@ describe('classifyDeployExtensionChanges', () => { }) describe('ensureDeployIdentifiersFromAppVersion', () => { + test('passes each module handle to a config reverse transform without changing its output', async () => { + const configuration: BaseConfigType & {synthetic_config: {enabled: boolean}} = {synthetic_config: {enabled: true}} + const reverse = vi.fn((content: object) => content) + const specification = createConfigExtensionSpecification({ + identifier: 'synthetic_config', + schema: BaseSchemaWithoutHandle.extend({synthetic_config: zod.object({enabled: zod.boolean()})}), + transformConfig: {forward: (content) => content, reverse}, + }) + const extension = new ExtensionInstance({ + configuration, + configurationPath: 'shopify.app.toml', + directory: '/app', + specification, + }) + extension.handle = 'Local_Module_Handle' + const app = testApp({ + ...APP, + allExtensions: [extension], + specifications: [specification], + }) + const remote: AppModuleVersion = { + registrationId: extension.uid, + registrationUuid: 'synthetic-uuid', + registrationTitle: 'Remote_Module_Handle', + type: specification.identifier, + config: configuration, + specification: { + identifier: specification.identifier, + name: 'Synthetic config', + experience: 'configuration', + options: {managementExperience: 'cli'}, + }, + } + + await ensureDeployIdentifiersFromAppVersion(deployOptions({app, activeAppVersion: {appModuleVersions: [remote]}})) + + expect(reverse).toHaveBeenCalledTimes(2) + expect(reverse).toHaveBeenNthCalledWith(1, configuration, { + flags: app.remoteFlags, + module: {handle: extension.handle}, + }) + expect(reverse).toHaveBeenNthCalledWith(2, configuration, { + flags: app.remoteFlags, + module: {handle: remote.registrationTitle}, + }) + expect(deployOrReleaseConfirmationPrompt).toHaveBeenLastCalledWith( + expect.objectContaining({ + configExtensionIdentifiersBreakdown: { + existingFieldNames: ['synthetic_config'], + existingUpdatedFieldNames: [], + newFieldNames: [], + deletedFieldNames: [], + }, + }), + ) + }) + test('prompts with the existing UI breakdown shape and returns deploy identifiers', async () => { const identifiers = await ensureDeployIdentifiersFromAppVersion( deployOptions({ diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.ts index 04236f1173d..4b2bbf2fb3b 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.ts @@ -189,8 +189,10 @@ async function localAppConfigurationExtensionContent(app: AppInterface, apiKey: // eslint-disable-next-line no-await-in-loop const deployConfig = await extension.deployConfig({apiKey, appConfiguration: app.configuration}) const localConfig = - extension.specification.transformRemoteToLocal?.(deployConfig ?? {}, {flags: app.remoteFlags}) ?? - extension.configuration + extension.specification.transformRemoteToLocal?.(deployConfig ?? {}, { + flags: app.remoteFlags, + module: {handle: extension.handle}, + }) ?? extension.configuration appConfig = deepMergeObjects(appConfig, localConfig) }