Skip to content

Commit afb85b0

Browse files
committed
Rename ApplicationModule to ExtensionInstance, old class to SpecificationBackedExtension
Phase 2 of the refactor: make ExtensionInstance the base class name. - Rename ApplicationModule class to ExtensionInstance (the base class) - Rename old ExtensionInstance class to SpecificationBackedExtension - Re-export ExtensionInstance from extension-instance.ts so all 76+ importing files continue working with zero changes - Update loader to use new SpecificationBackedExtension(...) - Update test files to use new SpecificationBackedExtension(...) - Replace direct .specification.X accesses with instance getters: .specification.identifier -> .type .specification.experience -> .experience .specification.buildConfig -> .buildConfig - Add optional specification? property on base class for backward compat 2088 tests pass across 209 files.
1 parent 8e4a90d commit afb85b0

File tree

14 files changed

+79
-63
lines changed

14 files changed

+79
-63
lines changed

packages/app/src/cli/models/app/app.test-data.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
OrganizationSource,
2020
} from '../organization.js'
2121
import {RemoteSpecification} from '../../api/graphql/extension_specifications.js'
22-
import {ExtensionInstance} from '../extensions/extension-instance.js'
22+
import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js'
2323
import {loadLocalExtensionsSpecifications} from '../extensions/load-specifications.js'
2424
import {FunctionConfigType} from '../extensions/specifications/function.js'
2525
import {BaseConfigType} from '../extensions/schemas.js'
@@ -278,7 +278,7 @@ export async function testUIExtension(
278278
const allSpecs = await loadLocalExtensionsSpecifications()
279279
const specification = allSpecs.find((spec) => spec.identifier === configuration.type)!
280280

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

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

327-
const extension = new ExtensionInstance({
327+
const extension = new SpecificationBackedExtension({
328328
configuration,
329329
configurationPath: 'shopify.app.toml',
330330
directory: directory ?? './',
@@ -354,7 +354,7 @@ export async function testAppAccessConfigExtension(
354354
},
355355
} as unknown as BaseConfigType)
356356

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

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

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

427-
const extension = new ExtensionInstance({
427+
const extension = new SpecificationBackedExtension({
428428
configuration,
429429
configurationPath: '',
430430
directory,
@@ -478,14 +478,14 @@ export async function testWebhookExtensions({emptyConfig = false, complianceTopi
478478
const webhooksSpecification = allSpecs.find((spec) => spec.identifier === 'webhooks')!
479479
const privacySpecification = allSpecs.find((spec) => spec.identifier === 'privacy_compliance_webhooks')!
480480

481-
const webhooksExtension = new ExtensionInstance({
481+
const webhooksExtension = new SpecificationBackedExtension({
482482
configuration,
483483
configurationPath: '',
484484
directory: './',
485485
specification: webhooksSpecification,
486486
})
487487

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

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

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

563-
const extension = new ExtensionInstance({
563+
const extension = new SpecificationBackedExtension({
564564
configuration,
565565
configurationPath: '',
566566
directory,
@@ -600,7 +600,7 @@ export async function testFunctionExtension(
600600
const allSpecs = await loadLocalExtensionsSpecifications()
601601
const specification = allSpecs.find((spec) => spec.identifier === 'function')!
602602

603-
const extension = new ExtensionInstance({
603+
const extension = new SpecificationBackedExtension({
604604
configuration,
605605
configurationPath: '',
606606
entryPath: opts.entryPath,
@@ -641,7 +641,7 @@ export async function testEditorExtensionCollection({
641641
}
642642
const configuration = parsed.data
643643

644-
return new ExtensionInstance({
644+
return new SpecificationBackedExtension({
645645
configuration,
646646
directory: resolvedDir,
647647
specification,
@@ -664,7 +664,7 @@ export async function testPaymentsAppExtension(
664664
const allSpecs = await loadLocalExtensionsSpecifications()
665665
const specification = allSpecs.find((spec) => spec.identifier === 'payments_extension')!
666666

667-
const extension = new ExtensionInstance({
667+
const extension = new SpecificationBackedExtension({
668668
configuration,
669669
configurationPath: '',
670670
entryPath: opts.entryPath,

packages/app/src/cli/models/app/app.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export class App<
330330

331331
get draftableExtensions() {
332332
return this.realExtensions.filter(
333-
(ext) => ext.isUUIDStrategyExtension || ext.specification.identifier === AppAccessSpecIdentifier,
333+
(ext) => ext.isUUIDStrategyExtension || ext.type === AppAccessSpecIdentifier,
334334
)
335335
}
336336

@@ -557,11 +557,11 @@ export function validateExtensionsHandlesInCollection(
557557
errors.push(
558558
`[${collection.handle}] editor extension collection: Add extension with handle '${extension.handle}' to local app. Local app must include extension with handle '${extension.handle}'.`,
559559
)
560-
} else if (!allowableTypesForExtensionInCollection.includes(matchingExtension.specification.identifier)) {
560+
} else if (!allowableTypesForExtensionInCollection.includes(matchingExtension.type)) {
561561
errors.push(
562-
`[${collection.handle}] editor extension collection: Remove extension of type '${matchingExtension.specification.identifier}' from this collection. This extension type is not supported in collections.`,
562+
`[${collection.handle}] editor extension collection: Remove extension of type '${matchingExtension.type}' from this collection. This extension type is not supported in collections.`,
563563
)
564-
} else if (matchingExtension.specification.identifier === 'ui_extension') {
564+
} else if (matchingExtension.type === 'ui_extension') {
565565
const uiExtension = matchingExtension as ExtensionInstance<UIExtensionType>
566566
uiExtension.configuration.extension_points.forEach((extensionPoint) => {
567567
if (extensionPoint.target.startsWith('admin.')) {

packages/app/src/cli/models/app/loader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from './config-file-naming.js'
2121
import {configurationFileNames, dotEnvFileNames} from '../../constants.js'
2222
import metadata from '../../metadata.js'
23-
import {ExtensionInstance} from '../extensions/extension-instance.js'
23+
import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js'
2424
import {ExtensionsArraySchema, UnifiedSchema} from '../extensions/schemas.js'
2525
import {ExtensionSpecification, isAppConfigSpecification} from '../extensions/specification.js'
2626
import {CreateAppOptions, Flag} from '../../utilities/developer-platform-client.js'
@@ -618,7 +618,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
618618
entryPath = await this.findEntryPath(directory, specification)
619619
}
620620

621-
const extensionInstance = new ExtensionInstance({
621+
const extensionInstance = new SpecificationBackedExtension({
622622
configuration,
623623
configurationPath,
624624
entryPath,

packages/app/src/cli/models/extensions/application-module.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,17 @@ export interface DevSessionWatchConfig {
6161
}
6262

6363
/**
64-
* Base class for all application modules (extensions and config modules).
64+
* Base class for all extension instances (extensions and config modules).
6565
*
6666
* Subclasses override behavior methods (appModuleFeatures, deployConfig, validate, etc.)
6767
* while the base class provides shared infrastructure (build pipeline, file watching,
6868
* handle/uid computation, bundling).
6969
*
70-
* This replaces the old ExtensionInstance + ExtensionSpecification composition pattern
71-
* with a cleaner inheritance model where per-type behavior lives in subclasses.
70+
* This is the single type used throughout the codebase for extension instances.
71+
* Legacy spec-based extensions use SpecificationBackedExtension (which extends this).
72+
* New module types extend this class directly with their own behavior.
7273
*/
73-
export abstract class ApplicationModule<TConfiguration extends BaseConfigType = BaseConfigType> {
74+
export abstract class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfigType> {
7475
entrySourceFilePath: string
7576
devUUID: string
7677
localIdentifier: string
@@ -83,6 +84,17 @@ export abstract class ApplicationModule<TConfiguration extends BaseConfigType =
8384
uid: string
8485
private cachedImportPaths?: string[]
8586

87+
/**
88+
* Legacy backward-compat property. Only available on SpecificationBackedExtension
89+
* instances. New ExtensionInstance subclasses don't have an ExtensionSpecification.
90+
*
91+
* Typed as `any` to avoid circular dependency with specification.ts.
92+
* Access through this property is discouraged — use the instance methods/getters
93+
* directly (e.g., ext.type, ext.experience, ext.buildConfig).
94+
*/
95+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
96+
specification?: any
97+
8698
constructor(options: {
8799
configuration: TConfiguration
88100
configurationPath: string

packages/app/src/cli/models/extensions/extension-instance.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ describe('draftMessages', async () => {
442442
const extensionInstance = await testAppConfigExtensions()
443443

444444
// Then
445-
expect(extensionInstance.handle).toBe(extensionInstance.specification.identifier)
445+
expect(extensionInstance.handle).toBe(extensionInstance.type)
446446
})
447447

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

472472
// Then
473-
expect(extensionInstance.uid).toBe(extensionInstance.specification.identifier)
473+
expect(extensionInstance.uid).toBe(extensionInstance.type)
474474
})
475475

476476
test('returns configuration uid when strategy is uuid and uid exists', async () => {

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import {BaseConfigType} from './schemas.js'
2-
import {ApplicationModule, ExtensionDeployConfigOptions} from './application-module.js'
2+
import {ExtensionInstance, ExtensionDeployConfigOptions} from './application-module.js'
33
import {ExtensionFeature, ExtensionSpecification, DevSessionWatchConfig} from './specification.js'
44
import {Flag} from '../../utilities/developer-platform-client.js'
55
import {AppConfiguration} from '../app/app.js'
66
import {ApplicationURLs} from '../../services/dev/urls.js'
77
import {ok, Result} from '@shopify/cli-kit/node/result'
88

9+
// Re-export ExtensionInstance from its canonical location so all existing
10+
// imports of `ExtensionInstance` from this file continue to work.
11+
export {ExtensionInstance} from './application-module.js'
12+
913
/**
10-
* Backward-compatible class that bridges the old ExtensionSpecification-based
11-
* composition pattern with the new ApplicationModule inheritance model.
14+
* Backward-compatible subclass that bridges the old ExtensionSpecification-based
15+
* composition pattern with the new ExtensionInstance inheritance model.
1216
*
13-
* This class extends ApplicationModule and delegates identity/behavior to the
14-
* existing ExtensionSpecification object, preserving the current API surface
15-
* for all 76+ consuming files.
17+
* This class extends ExtensionInstance and delegates identity/behavior to the
18+
* existing ExtensionSpecification object, preserving the current creation flow
19+
* in the loader and test helpers.
1620
*
17-
* Once all specs are migrated to ApplicationModule subclasses, this class
18-
* will be removed and consumers will use ApplicationModule directly.
21+
* Once all specs are migrated to ExtensionInstance subclasses, this class
22+
* will be removed.
1923
*/
20-
export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfigType> extends ApplicationModule<TConfiguration> {
24+
export class SpecificationBackedExtension<TConfiguration extends BaseConfigType = BaseConfigType> extends ExtensionInstance<TConfiguration> {
2125
specification: ExtensionSpecification
2226

2327
constructor(options: {

packages/app/src/cli/models/extensions/module-descriptor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import {ZodSchemaType, BaseConfigType} from './schemas.js'
2-
import {ApplicationModule, ExtensionExperience, UidStrategy} from './application-module.js'
2+
import {ExtensionInstance, ExtensionExperience, UidStrategy} from './application-module.js'
33
import {RemoteSpecification} from '../../api/graphql/extension_specifications.js'
44
import {ParseConfigurationResult} from '@shopify/cli-kit/node/schema'
55

66
/**
77
* Pre-instantiation metadata for an application module type.
88
*
99
* This interface replaces ExtensionSpecification for concerns that must be
10-
* resolved BEFORE creating an ApplicationModule instance:
10+
* resolved BEFORE creating an ExtensionInstance instance:
1111
* - Type identification (identifier lookup)
1212
* - Configuration schema and parsing
1313
* - App configuration schema contribution
1414
* - Instance creation (factory method)
1515
*
1616
* Unlike ExtensionSpecification, this interface has NO instance behavior.
17-
* All instance behavior lives on ApplicationModule subclasses.
17+
* All instance behavior lives on ExtensionInstance subclasses.
1818
*
1919
* Each module type (function, ui_extension, theme, etc.) provides one
2020
* ModuleDescriptor that is registered in the ModuleRegistry.
@@ -63,7 +63,7 @@ export interface ModuleDescriptor<TConfiguration extends BaseConfigType = BaseCo
6363
parseConfigurationObject: (configurationObject: object) => ParseConfigurationResult<TConfiguration>
6464

6565
/**
66-
* Factory: create the correct ApplicationModule subclass for this type.
66+
* Factory: create the correct ExtensionInstance subclass for this type.
6767
* The descriptor knows which class to instantiate and passes through
6868
* the construction options.
6969
*/
@@ -73,5 +73,5 @@ export interface ModuleDescriptor<TConfiguration extends BaseConfigType = BaseCo
7373
entryPath?: string
7474
directory: string
7575
remoteSpec: RemoteSpecification
76-
}) => ApplicationModule<TConfiguration>
76+
}) => ExtensionInstance<TConfiguration>
7777
}

packages/app/src/cli/models/extensions/specifications/editor_extension_collection.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as loadLocales from '../../../utilities/extensions/locales-configuration.js'
2-
import {ExtensionInstance} from '../extension-instance.js'
2+
import {SpecificationBackedExtension} from '../extension-instance.js'
33
import {loadLocalExtensionsSpecifications} from '../load-specifications.js'
44
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
55
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
@@ -32,7 +32,7 @@ describe('editor_extension_collection', async () => {
3232

3333
const config = parsed.data
3434

35-
return new ExtensionInstance({
35+
return new SpecificationBackedExtension({
3636
configuration: config,
3737
directory,
3838
specification,

0 commit comments

Comments
 (0)