Skip to content
Draft
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
32 changes: 16 additions & 16 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
OrganizationSource,
} from '../organization.js'
import {RemoteSpecification} from '../../api/graphql/extension_specifications.js'
import {ExtensionInstance} from '../extensions/extension-instance.js'
import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js'
import {loadLocalExtensionsSpecifications} from '../extensions/load-specifications.js'
import {FunctionConfigType} from '../extensions/specifications/function.js'
import {BaseConfigType} from '../extensions/schemas.js'
Expand Down Expand Up @@ -278,7 +278,7 @@ export async function testUIExtension(
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === configuration.type)!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: configuration as BaseConfigType,
configurationPath,
entryPath,
Expand All @@ -302,7 +302,7 @@ export async function testThemeExtensions(directory = './my-extension'): Promise
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'theme')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory,
Expand All @@ -324,7 +324,7 @@ export async function testAppConfigExtensions(emptyConfig = false, directory?: s
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'point_of_sale')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: 'shopify.app.toml',
directory: directory ?? './',
Expand Down Expand Up @@ -354,7 +354,7 @@ export async function testAppAccessConfigExtension(
},
} as unknown as BaseConfigType)

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: 'shopify.app.toml',
directory: directory ?? './',
Expand All @@ -377,7 +377,7 @@ export async function testAppHomeConfigExtension(): Promise<ExtensionInstance> {
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === AppHomeSpecIdentifier)!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory: './',
Expand All @@ -403,7 +403,7 @@ export async function testAppProxyConfigExtension(): Promise<ExtensionInstance>
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === AppProxySpecIdentifier)!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory: './',
Expand All @@ -424,7 +424,7 @@ export async function testPaymentExtensions(directory = './my-extension'): Promi
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'payments_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory,
Expand Down Expand Up @@ -478,14 +478,14 @@ export async function testWebhookExtensions({emptyConfig = false, complianceTopi
const webhooksSpecification = allSpecs.find((spec) => spec.identifier === 'webhooks')!
const privacySpecification = allSpecs.find((spec) => spec.identifier === 'privacy_compliance_webhooks')!

const webhooksExtension = new ExtensionInstance({
const webhooksExtension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory: './',
specification: webhooksSpecification,
})

const privacyExtension = new ExtensionInstance({
const privacyExtension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory: './',
Expand All @@ -512,7 +512,7 @@ export async function testSingleWebhookSubscriptionExtension({
// we create the extension instances in loader
const configuration = emptyConfig ? ({} as unknown as BaseConfigType) : (config as unknown as BaseConfigType)

const webhooksExtension = new ExtensionInstance({
const webhooksExtension = new SpecificationBackedExtension({
configuration,
configurationPath: 'shopify.app.toml',
directory: './',
Expand All @@ -539,7 +539,7 @@ export async function testTaxCalculationExtension(directory = './my-extension'):
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'tax_calculation')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory,
Expand All @@ -560,7 +560,7 @@ export async function testFlowActionExtension(directory = './my-extension'): Pro
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'flow_action')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
directory,
Expand Down Expand Up @@ -600,7 +600,7 @@ export async function testFunctionExtension(
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'function')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
entryPath: opts.entryPath,
Expand Down Expand Up @@ -641,7 +641,7 @@ export async function testEditorExtensionCollection({
}
const configuration = parsed.data

return new ExtensionInstance({
return new SpecificationBackedExtension({
configuration,
directory: resolvedDir,
specification,
Expand All @@ -664,7 +664,7 @@ export async function testPaymentsAppExtension(
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'payments_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration,
configurationPath: '',
entryPath: opts.entryPath,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/models/app/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ describe('validateFunctionExtensionsWithUiHandle', () => {
}
const editorExtensionCollection = (await testEditorExtensionCollection({
configuration,
})) as ExtensionInstance<EditorExtensionCollectionType>
})) as unknown as ExtensionInstance<EditorExtensionCollectionType>

const orderDiscountFunction = await testFunctionExtension({
config: {
Expand Down
10 changes: 4 additions & 6 deletions packages/app/src/cli/models/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,7 @@ export class App<
}

get draftableExtensions() {
return this.realExtensions.filter(
(ext) => ext.isUUIDStrategyExtension || ext.specification.identifier === AppAccessSpecIdentifier,
)
return this.realExtensions.filter((ext) => ext.isUUIDStrategyExtension || ext.type === AppAccessSpecIdentifier)
}

setDevApplicationURLs(devApplicationURLs: ApplicationURLs) {
Expand Down Expand Up @@ -563,11 +561,11 @@ export function validateExtensionsHandlesInCollection(
errors.push(
`[${collection.handle}] editor extension collection: Add extension with handle '${extension.handle}' to local app. Local app must include extension with handle '${extension.handle}'.`,
)
} else if (!allowableTypesForExtensionInCollection.includes(matchingExtension.specification.identifier)) {
} else if (!allowableTypesForExtensionInCollection.includes(matchingExtension.type)) {
errors.push(
`[${collection.handle}] editor extension collection: Remove extension of type '${matchingExtension.specification.identifier}' from this collection. This extension type is not supported in collections.`,
`[${collection.handle}] editor extension collection: Remove extension of type '${matchingExtension.type}' from this collection. This extension type is not supported in collections.`,
)
} else if (matchingExtension.specification.identifier === 'ui_extension') {
} else if (matchingExtension.type === 'ui_extension') {
const uiExtension = matchingExtension as ExtensionInstance<UIExtensionType>
uiExtension.configuration.extension_points.forEach((extensionPoint) => {
if (extensionPoint.target.startsWith('admin.')) {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/cli/models/app/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from './config-file-naming.js'
import {configurationFileNames, dotEnvFileNames} from '../../constants.js'
import metadata from '../../metadata.js'
import {ExtensionInstance} from '../extensions/extension-instance.js'
import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js'
import {ExtensionsArraySchema, UnifiedSchema} from '../extensions/schemas.js'
import {ExtensionSpecification, isAppConfigSpecification} from '../extensions/specification.js'
import {CreateAppOptions, Flag} from '../../utilities/developer-platform-client.js'
Expand Down Expand Up @@ -635,7 +635,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
entryPath = await this.findEntryPath(directory, specification)
}

const extensionInstance = new ExtensionInstance({
const extensionInstance = new SpecificationBackedExtension({
configuration,
configurationPath,
entryPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export interface DevSessionWatchConfig {
* while the base class provides shared infrastructure (build pipeline, file watching,
* handle/uid computation, bundling).
*
* This replaces the old ExtensionInstance + ExtensionSpecification composition pattern
* with a cleaner inheritance model where per-type behavior lives in subclasses.
* Legacy spec-based extensions use SpecificationBackedExtension (which extends this).
* New module types extend this class directly with their own behavior.
*/
export abstract class ApplicationModule<TConfiguration extends BaseConfigType = BaseConfigType> {
entrySourceFilePath: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ describe('draftMessages', async () => {
const extensionInstance = await testAppConfigExtensions()

// Then
expect(extensionInstance.handle).toBe(extensionInstance.specification.identifier)
expect(extensionInstance.handle).toBe(extensionInstance.type)
})

test('extensions handle is a hashString when specification uidStrategy is dynamic and it is a webhook subscription extension', async () => {
Expand Down Expand Up @@ -470,7 +470,7 @@ describe('draftMessages', async () => {
const extensionInstance = await testAppConfigExtensions()

// Then
expect(extensionInstance.uid).toBe(extensionInstance.specification.identifier)
expect(extensionInstance.uid).toBe(extensionInstance.type)
})

test('returns configuration uid when strategy is uuid and uid exists', async () => {
Expand Down
27 changes: 20 additions & 7 deletions packages/app/src/cli/models/extensions/extension-instance.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import {BaseConfigType} from './schemas.js'
import {ApplicationModule, ExtensionDeployConfigOptions} from './application-module.js'
import {joinPath} from '@shopify/cli-kit/node/path'
import {ExtensionFeature, ExtensionSpecification, DevSessionWatchConfig} from './specification.js'
import {Flag} from '../../utilities/developer-platform-client.js'
import {AppConfiguration} from '../app/app.js'
import {ApplicationURLs} from '../../services/dev/urls.js'
import {joinPath} from '@shopify/cli-kit/node/path'
import {ok, Result} from '@shopify/cli-kit/node/result'

// Re-export ApplicationModule as ExtensionInstance for backward compatibility.
// All 76+ files that import ExtensionInstance continue to work as a type.

export type ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfigType> =
ApplicationModule<TConfiguration>

/**
* Backward-compatible class that bridges the old ExtensionSpecification-based
* Backward-compatible subclass that bridges the old ExtensionSpecification-based
* composition pattern with the new ApplicationModule inheritance model.
*
* This class extends ApplicationModule and delegates identity/behavior to the
* existing ExtensionSpecification object, preserving the current API surface
* for all 76+ consuming files.
* existing ExtensionSpecification object, preserving the current creation flow
* in the loader and test helpers.
*
* Once all specs are migrated to ApplicationModule subclasses, this class
* will be removed and consumers will use ApplicationModule directly.
* will be removed.
*/
export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfigType> extends ApplicationModule<TConfiguration> {
export class SpecificationBackedExtension<
TConfiguration extends BaseConfigType = BaseConfigType,
> extends ApplicationModule<TConfiguration> {
specification: ExtensionSpecification

constructor(options: {
Expand Down Expand Up @@ -116,7 +124,12 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
apiKey,
appConfiguration,
}: ExtensionDeployConfigOptions): Promise<{[key: string]: unknown} | undefined> {
const deployConfigResult = await this.specification.deployConfig?.(this.configuration, this.directory, apiKey, undefined)
const deployConfigResult = await this.specification.deployConfig?.(
this.configuration,
this.directory,
apiKey,
undefined,
)
const transformedConfig = this.specification.transformLocalToRemote?.(this.configuration, appConfiguration) as
| {[key: string]: unknown}
| undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as loadLocales from '../../../utilities/extensions/locales-configuration.js'
import {ExtensionInstance} from '../extension-instance.js'
import {SpecificationBackedExtension} from '../extension-instance.js'
import {loadLocalExtensionsSpecifications} from '../load-specifications.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('editor_extension_collection', async () => {

const config = parsed.data

return new ExtensionInstance({
return new SpecificationBackedExtension({
configuration: config,
directory,
specification,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {getShouldRenderTarget} from './ui_extension.js'
import * as loadLocales from '../../../utilities/extensions/locales-configuration.js'
import {ExtensionInstance} from '../extension-instance.js'
import {SpecificationBackedExtension} from '../extension-instance.js'
import {loadLocalExtensionsSpecifications} from '../load-specifications.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {AssetIdentifier} from '../specification.js'
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('ui_extension', async () => {
urls: {},
}

return new ExtensionInstance({
return new SpecificationBackedExtension({
configuration,
directory,
specification,
Expand Down Expand Up @@ -1003,7 +1003,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const configurationPath = joinPath(tmpDir, 'shopify.extension.toml')
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const uiExtension = new ExtensionInstance({
const uiExtension = new SpecificationBackedExtension({
configuration: {
extension_points: [],
api_version: '2023-01' as const,
Expand Down Expand Up @@ -1042,7 +1042,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const configurationPath = joinPath(tmpDir, 'shopify.extension.toml')
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const uiExtension = new ExtensionInstance({
const uiExtension = new SpecificationBackedExtension({
configuration: {
extension_points: [],
api_version: '2023-01' as const,
Expand Down Expand Up @@ -1081,7 +1081,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const configurationPath = joinPath(tmpDir, 'shopify.extension.toml')
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!
const uiExtension = new ExtensionInstance({
const uiExtension = new SpecificationBackedExtension({
configuration: {
extension_points: [],
api_version: '2023-01' as const,
Expand Down Expand Up @@ -1346,7 +1346,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: {
api_version: apiVersion,
extension_points: [
Expand Down Expand Up @@ -1814,7 +1814,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: {
api_version: '2025-10',
extension_points: [
Expand Down Expand Up @@ -1889,7 +1889,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: {
// Remote DOM supported version
api_version: '2025-10',
Expand Down Expand Up @@ -1984,7 +1984,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: {
// Remote DOM supported version
api_version: '2025-10',
Expand Down Expand Up @@ -2088,7 +2088,7 @@ Please check the configuration in ${joinPath(tmpDir, 'shopify.extension.toml')}`
const allSpecs = await loadLocalExtensionsSpecifications()
const specification = allSpecs.find((spec) => spec.identifier === 'ui_extension')!

const extension = new ExtensionInstance({
const extension = new SpecificationBackedExtension({
configuration: {
// Remote DOM supported version
api_version: '2025-10',
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/services/app-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ describe('localAppContext', () => {
})

// Then
const realExtensions = result.app.allExtensions.filter((ext) => ext.specification.experience !== 'configuration')
const realExtensions = result.app.allExtensions.filter((ext) => ext.experience !== 'configuration')
expect(realExtensions).toHaveLength(1)
expect(realExtensions[0]).toEqual(
expect.objectContaining({
Expand Down
Loading
Loading