From fc1a187414b786b503930a9ae36bc0e2ee12b9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 9 Jul 2026 19:19:55 +0200 Subject: [PATCH] Make deploy consumers speak app-module UUID/registration-ID maps Introduces `ExtensionUuidsByLocalIdentifier` and `DeployIdentifiers` and switches the manifest and bundling consumers to plain UUID-by-local-identifier maps: `app.manifest`, `ExtensionInstance.bundleConfig`, and `bundleAndBuildExtensions` no longer take the full `Identifiers` object (the always-undefined dev-dashboard `outputId` plumbing is removed with it). `deploy` still derives identifiers from the existing matching flow and adapts them into these maps, so behavior is unchanged. Later commits replace the matching flow itself. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/app/src/cli/models/app/app.test.ts | 7 +- packages/app/src/cli/models/app/app.ts | 8 +- .../app/src/cli/models/app/identifiers.ts | 4 + .../extensions/extension-instance.test.ts | 25 +--- .../models/extensions/extension-instance.ts | 27 ++--- packages/app/src/cli/services/deploy.ts | 16 ++- .../src/cli/services/deploy/bundle.test.ts | 108 +++--------------- .../app/src/cli/services/deploy/bundle.ts | 10 -- 8 files changed, 51 insertions(+), 154 deletions(-) diff --git a/packages/app/src/cli/models/app/app.test.ts b/packages/app/src/cli/models/app/app.test.ts index ed48a11f709..89c4e4ffd64 100644 --- a/packages/app/src/cli/models/app/app.test.ts +++ b/packages/app/src/cli/models/app/app.test.ts @@ -651,12 +651,7 @@ describe('manifest', () => { }) // When - const manifest = await app.manifest({ - app: 'API_KEY', - extensions: {app_access: 'UUID_A'}, - extensionIds: {}, - extensionsNonUuidManaged: {}, - }) + const manifest = await app.manifest({app_access: 'UUID_A'}) // Then expect(manifest).toEqual({ diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index c2fa8ddf53a..73eec01f3fc 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -1,6 +1,6 @@ import {AppErrors, isWebType} from './loader.js' import {ensurePathStartsWithSlash} from './validation/common.js' -import {Identifiers} from './identifiers.js' +import {ExtensionUuidsByLocalIdentifier} from './identifiers.js' import {ExtensionInstance} from '../extensions/extension-instance.js' import {FunctionConfigType} from '../extensions/specifications/function.js' import {ExtensionSpecification, RemoteAwareExtensionSpecification} from '../extensions/specification.js' @@ -245,7 +245,7 @@ export interface AppInterface< * If creating an app on the platform based on this app and its configuration, what default options should the app take? */ creationDefaultOptions(): CreateAppOptions - manifest: (identifiers: Identifiers | undefined) => Promise + manifest: (appModuleUuids: ExtensionUuidsByLocalIdentifier | undefined) => Promise removeExtension: (extensionUid: string) => void updateHiddenConfig: (values: Partial) => Promise setDevApplicationURLs: (devApplicationURLs: ApplicationURLs) => void @@ -331,7 +331,7 @@ export class App< this.realExtensions.forEach((ext) => ext.patchWithAppDevURLs(devApplicationURLs)) } - async manifest(identifiers: Identifiers | undefined): Promise { + async manifest(appModuleUuids: ExtensionUuidsByLocalIdentifier | undefined): Promise { const modules = await Promise.all( this.realExtensions.map(async (module) => { const config = await module.deployConfig({ @@ -342,7 +342,7 @@ export class App< type: module.externalType, handle: module.handle, uid: module.uid, - uuid: identifiers?.extensions[module.localIdentifier], + uuid: appModuleUuids?.[module.localIdentifier], assets: module.uid, target: module.contextValue, config: (config ?? {}) as JsonMapType, diff --git a/packages/app/src/cli/models/app/identifiers.ts b/packages/app/src/cli/models/app/identifiers.ts index ed38e1ecdb0..d0caebcf0f8 100644 --- a/packages/app/src/cli/models/app/identifiers.ts +++ b/packages/app/src/cli/models/app/identifiers.ts @@ -30,6 +30,10 @@ export interface Identifiers { extensionsNonUuidManaged: IdentifiersExtensions } +export interface ExtensionUuidsByLocalIdentifier { + [localIdentifier: string]: string +} + type UuidOnlyIdentifiers = Omit type UpdateAppIdentifiersCommand = 'dev' | 'deploy' | 'release' | 'import-extensions' interface UpdateAppIdentifiersOptions { diff --git a/packages/app/src/cli/models/extensions/extension-instance.test.ts b/packages/app/src/cli/models/extensions/extension-instance.test.ts index 65cedcc3a0c..99191649959 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.test.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.test.ts @@ -233,16 +233,11 @@ describe('deployConfig', async () => { }) describe('bundleConfig', async () => { - test('returns the uuid from extensions when the extension is uuid managed', async () => { + test('returns the uuid from appModuleUuids', async () => { const extensionInstance = await testThemeExtensions() const got = await extensionInstance.bundleConfig({ - identifiers: { - extensions: {'theme-extension-name': 'theme-uuid'}, - extensionIds: {}, - app: 'My app', - extensionsNonUuidManaged: {}, - }, + appModuleUuids: {'theme-extension-name': 'theme-uuid'}, developerPlatformClient, apiKey: 'apiKey', appConfiguration: placeholderAppConfiguration, @@ -260,12 +255,7 @@ describe('bundleConfig', async () => { const extensionInstance = await testPaymentExtensions() const got = await extensionInstance.bundleConfig({ - identifiers: { - extensions: {'payment-extension-name': 'payment-uuid'}, - extensionIds: {}, - app: 'My app', - extensionsNonUuidManaged: {}, - }, + appModuleUuids: {'payment-extension-name': 'payment-uuid'}, developerPlatformClient, apiKey: 'apiKey', appConfiguration: placeholderAppConfiguration, @@ -279,16 +269,11 @@ describe('bundleConfig', async () => { ) }) - test('returns the uuid from extensionsNonUuidManaged when the extension is not uuid managed', async () => { + test('returns the uuid for a non-uuid-managed extension', async () => { const extensionInstance = await testAppConfigExtensions() const got = await extensionInstance.bundleConfig({ - identifiers: { - extensions: {}, - extensionIds: {}, - app: 'My app', - extensionsNonUuidManaged: {point_of_sale: 'uuid'}, - }, + appModuleUuids: {point_of_sale: 'uuid'}, developerPlatformClient, apiKey: 'apiKey', appConfiguration: placeholderAppConfiguration, diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index dcd6349515f..23ae5cb345c 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -3,7 +3,7 @@ import {FunctionConfigType} from './specifications/function.js' import {DevSessionWatchConfig, ExtensionFeature, ExtensionSpecification} from './specification.js' import {SingleWebhookSubscriptionType} from './specifications/app_config_webhook_schemas/webhooks_schema.js' import {ExtensionBuildOptions} from '../../services/build/extension.js' -import {Identifiers} from '../app/identifiers.js' +import {ExtensionUuidsByLocalIdentifier} from '../app/identifiers.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {AppConfiguration} from '../app/app.js' import {ApplicationURLs} from '../../services/dev/urls.js' @@ -245,10 +245,8 @@ export class ExtensionInstance { const appModules = await Promise.all( app.allExtensions.flatMap((ext) => - ext.bundleConfig({identifiers, developerPlatformClient, apiKey, appConfiguration: app.configuration}), + ext.bundleConfig({appModuleUuids, developerPlatformClient, apiKey, appConfiguration: app.configuration}), ), ) @@ -346,3 +346,11 @@ async function outputCompletionMessage({ ], }) } + +function appManifestUuids(app: AppLinkedInterface, appModuleUuids: {[localIdentifier: string]: string}) { + return Object.fromEntries( + app.allExtensions + .filter((extension) => extension.isUUIDStrategyExtension) + .map((extension) => [extension.localIdentifier, appModuleUuids[extension.localIdentifier]!]), + ) +} diff --git a/packages/app/src/cli/services/deploy/bundle.test.ts b/packages/app/src/cli/services/deploy/bundle.test.ts index 941194362d8..e1253552561 100644 --- a/packages/app/src/cli/services/deploy/bundle.test.ts +++ b/packages/app/src/cli/services/deploy/bundle.test.ts @@ -33,26 +33,14 @@ describe('bundleAndBuildExtensions', () => { themeExtension.buildForBundle = extensionBundleMock app = testApp({allExtensions: [uiExtension, themeExtension], directory: tmpDir}) - const extensions: {[key: string]: string} = {} - for (const extension of app.allExtensions) { - extensions[extension.localIdentifier] = extension.localIdentifier - } - const identifiers = { - app: 'app-id', - extensions, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: false, - isDevDashboardApp: false, }) // Then @@ -75,26 +63,14 @@ describe('bundleAndBuildExtensions', () => { functionExtension.buildForBundle = extensionBundleMock const app = testApp({allExtensions: [functionExtension], directory: tmpDir}) - const extensions: {[key: string]: string} = {} - for (const extension of app.allExtensions) { - extensions[extension.localIdentifier] = extension.localIdentifier - } - const identifiers = { - app: 'app-id', - extensions, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: false, - isDevDashboardApp: false, }) // Then @@ -114,33 +90,20 @@ describe('bundleAndBuildExtensions', () => { functionExtension.buildForBundle = extensionBuildMock const app = testApp({allExtensions: [functionExtension], directory: tmpDir}) - const extensions: {[key: string]: string} = {} - for (const extension of app.allExtensions) { - extensions[extension.localIdentifier] = extension.localIdentifier - } - const identifiers = { - app: 'app-id', - extensions, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: true, - isDevDashboardApp: false, }) // Then expect(extensionBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - functionExtension.localIdentifier, ) await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() }) @@ -159,22 +122,14 @@ describe('bundleAndBuildExtensions', () => { functionExtension.buildForBundle = extensionBuildMock const app = testApp({allExtensions: [functionExtension], directory: tmpDir}) - const identifiers = { - app: 'app-id', - extensions: {[functionExtension.localIdentifier]: functionExtension.localIdentifier}, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: true, - isDevDashboardApp: false, }) // Then @@ -182,7 +137,6 @@ describe('bundleAndBuildExtensions', () => { expect(extensionBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - functionExtension.localIdentifier, ) }) }) @@ -200,22 +154,14 @@ describe('bundleAndBuildExtensions', () => { functionExtension.buildForBundle = extensionBuildMock const app = testApp({allExtensions: [functionExtension], directory: tmpDir}) - const identifiers = { - app: 'app-id', - extensions: {[functionExtension.localIdentifier]: functionExtension.localIdentifier}, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: false, - isDevDashboardApp: false, }) // Then @@ -238,29 +184,20 @@ describe('bundleAndBuildExtensions', () => { const app = testApp({allExtensions: [themeExtension], directory: tmpDir}) - const identifiers = { - app: 'app-id', - extensions: {[themeExtension.localIdentifier]: themeExtension.localIdentifier}, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: true, - isDevDashboardApp: false, }) // Then expect(extensionBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - themeExtension.localIdentifier, ) await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() }) @@ -298,42 +235,27 @@ describe('bundleAndBuildExtensions', () => { directory: tmpDir, }) - const extensions: {[key: string]: string} = {} - for (const extension of app.allExtensions) { - extensions[extension.localIdentifier] = extension.localIdentifier - } - const identifiers = { - app: 'app-id', - extensions, - extensionIds: {}, - extensionsNonUuidManaged: {}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: true, - isDevDashboardApp: false, }) expect(functionBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - functionExtension.localIdentifier, ) expect(themeBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - themeExtension.localIdentifier, ) expect(uiBuildMock).toHaveBeenCalledWith( expect.objectContaining({app, environment: 'production', skipBuild: true}), joinPath(tmpDir, '.shopify', 'deploy-bundle'), - uiExtension.localIdentifier, ) await expect(file.fileExists(bundlePath)).resolves.toBeTruthy() @@ -349,22 +271,14 @@ describe('bundleAndBuildExtensions', () => { const configExtension = await testAppConfigExtensions() const app = testApp({allExtensions: [configExtension], directory: tmpDir}) - const identifiers = { - app: 'app-id', - extensions: {}, - extensionIds: {}, - extensionsNonUuidManaged: {[configExtension.localIdentifier]: configExtension.localIdentifier}, - } - appManifest = await app.manifest(identifiers) + appManifest = await app.manifest(appModuleUuidsFor(app)) // When const result = await bundleAndBuildExtensions({ app, appManifest, - identifiers, bundlePath, skipBuild: false, - isDevDashboardApp: false, }) // Then @@ -374,3 +288,9 @@ describe('bundleAndBuildExtensions', () => { }) }) }) + +function appModuleUuidsFor(app: AppInterface) { + return Object.fromEntries( + app.allExtensions.map((extension) => [extension.localIdentifier, extension.localIdentifier]), + ) +} diff --git a/packages/app/src/cli/services/deploy/bundle.ts b/packages/app/src/cli/services/deploy/bundle.ts index 1f8e482d952..e0a5bd83bcf 100644 --- a/packages/app/src/cli/services/deploy/bundle.ts +++ b/packages/app/src/cli/services/deploy/bundle.ts @@ -1,5 +1,4 @@ import {AppInterface, AppManifest} from '../../models/app/app.js' -import {Identifiers} from '../../models/app/identifiers.js' import {installJavy} from '../function/build.js' import {compressBundle, writeManifestToBundle} from '../bundle.js' import {AbortSignal} from '@shopify/cli-kit/node/abort' @@ -12,9 +11,7 @@ interface BundleOptions { app: AppInterface appManifest: AppManifest bundlePath: string - identifiers?: Identifiers skipBuild: boolean - isDevDashboardApp: boolean } /** @@ -39,16 +36,9 @@ export async function bundleAndBuildExtensions(options: BundleOptions): Promise< const extensionBuildProcesses = options.app.allExtensions.map((extension) => ({ prefix: extension.localIdentifier, action: async (stdout: Writable, stderr: Writable, signal: AbortSignal) => { - // This outputId is the UID for AppManagement, and UUID for Partners - // Comes from the matching logic in `ensureDeployContext` - const outputId = options.isDevDashboardApp - ? undefined - : options.identifiers?.extensions[extension.localIdentifier] - await extension.buildForBundle( {stderr, stdout, signal, app: options.app, environment: 'production', skipBuild: options.skipBuild}, bundleDirectory, - outputId, ) }, }))