Skip to content

Commit f51e497

Browse files
committed
Wire loader to support ModuleRegistry alongside ExtensionSpecification
Phase 3a: Add dual-path instance creation to the AppLoader. - Add ModuleRegistry field to AppLoader - Update createExtensionInstance to check registry first via descriptor.createModule(), falling back to legacy spec path - Add createModuleFromDescriptor method for the new path - Widen findEntryPath signature to accept both specs and descriptors - Add moduleRegistry to ConfigurationLoaderResult type The registry is currently empty so all instances still use the legacy SpecificationBackedExtension path. Next PR will migrate actual specs.
1 parent 314e17b commit f51e497

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

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

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
import {configurationFileNames, dotEnvFileNames} from '../../constants.js'
2222
import metadata from '../../metadata.js'
2323
import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js'
24+
import {ModuleRegistry} from '../extensions/module-registry.js'
25+
import {loadModuleRegistry} from '../extensions/load-specifications.js'
2426
import {ExtensionsArraySchema, UnifiedSchema} from '../extensions/schemas.js'
2527
import {ExtensionSpecification, isAppConfigSpecification} from '../extensions/specification.js'
2628
import {CreateAppOptions, Flag} from '../../utilities/developer-platform-client.js'
@@ -327,6 +329,23 @@ export async function loadAppFromContext<TModuleSpec extends ExtensionSpecificat
327329
usesCliManagedUrls: configuration.build?.automatically_update_urls_on_dev,
328330
}
329331

332+
// Build the module registry and merge remote spec values into it so
333+
// descriptors have up-to-date identity data from the platform API.
334+
const moduleRegistry = loadModuleRegistry()
335+
const remoteSpecsForMerge = specifications.map((spec) => ({
336+
name: spec.externalName,
337+
externalName: spec.externalName,
338+
identifier: spec.identifier,
339+
gated: false,
340+
externalIdentifier: spec.externalIdentifier,
341+
experience: spec.experience as 'extension' | 'configuration' | 'deprecated',
342+
managementExperience: 'cli' as const,
343+
registrationLimit: spec.registrationLimit,
344+
uidStrategy: spec.uidStrategy,
345+
surface: spec.surface,
346+
}))
347+
moduleRegistry.mergeRemoteSpecs(remoteSpecsForMerge)
348+
330349
const loadedConfiguration: ConfigurationLoaderResult<CurrentAppConfiguration, TModuleSpec> = {
331350
directory: project.directory,
332351
configPath: configurationPath,
@@ -335,6 +354,7 @@ export async function loadAppFromContext<TModuleSpec extends ExtensionSpecificat
335354
configSchema,
336355
specifications,
337356
remoteFlags,
357+
moduleRegistry,
338358
}
339359

340360
const loader = new AppLoader<CurrentAppConfiguration, TModuleSpec>({
@@ -466,6 +486,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
466486
private readonly loadedConfiguration: ConfigurationLoaderResult<TConfig, TModuleSpec>
467487
private readonly reloadState: ReloadState | undefined
468488
private readonly project: Project
489+
private readonly moduleRegistry: ModuleRegistry
469490

470491
constructor({
471492
ignoreUnknownExtensions,
@@ -479,6 +500,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
479500
this.loadedConfiguration = loadedConfiguration
480501
this.reloadState = reloadState
481502
this.project = project
503+
this.moduleRegistry = loadedConfiguration.moduleRegistry ?? new ModuleRegistry()
482504
}
483505

484506
private get activeConfigFile(): TomlFile | undefined {
@@ -594,6 +616,14 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
594616
configurationPath: string,
595617
directory: string,
596618
): Promise<ExtensionInstance | undefined> {
619+
// Check the module registry first (new subclass-based path).
620+
// If a descriptor is found, use its factory to create the right subclass.
621+
const descriptor = this.moduleRegistry.findForType(type)
622+
if (descriptor) {
623+
return this.createModuleFromDescriptor(descriptor, configurationObject, configurationPath, directory)
624+
}
625+
626+
// Fall back to legacy ExtensionSpecification path.
597627
const specification = this.findSpecificationForType(type)
598628
let entryPath
599629
let usedKnownSpecification = false
@@ -649,6 +679,62 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
649679
return extensionInstance
650680
}
651681

682+
private async createModuleFromDescriptor(
683+
descriptor: ReturnType<ModuleRegistry['findForType']> & {},
684+
configurationObject: object,
685+
configurationPath: string,
686+
directory: string,
687+
): Promise<ExtensionInstance | undefined> {
688+
const parseResult = descriptor.parseConfigurationObject(configurationObject)
689+
if (parseResult.state === 'error') {
690+
if (parseResult.errors) {
691+
for (const error of parseResult.errors) {
692+
this.errors.addError({
693+
file: configurationPath,
694+
message: error.message ?? `Validation error at ${error.path.join('.')}`,
695+
})
696+
}
697+
}
698+
return undefined
699+
}
700+
701+
const configuration = parseResult.data
702+
const entryPath = await this.findEntryPath(directory, descriptor)
703+
704+
const moduleInstance = descriptor.createModule({
705+
configuration,
706+
configurationPath,
707+
entryPath,
708+
directory,
709+
remoteSpec: {
710+
name: descriptor.externalName,
711+
externalName: descriptor.externalName,
712+
identifier: descriptor.identifier,
713+
gated: false,
714+
externalIdentifier: descriptor.externalIdentifier,
715+
experience: descriptor.experience,
716+
managementExperience: 'cli',
717+
registrationLimit: descriptor.registrationLimit,
718+
uidStrategy: descriptor.uidStrategy,
719+
surface: descriptor.surface,
720+
},
721+
})
722+
723+
if (this.reloadState && configuration.handle) {
724+
const previousDevUUID = this.reloadState.extensionDevUUIDs.get(configuration.handle)
725+
if (previousDevUUID) {
726+
moduleInstance.devUUID = previousDevUUID
727+
}
728+
}
729+
730+
const validateResult = await moduleInstance.validate()
731+
if (validateResult.isErr()) {
732+
this.errors.addError({file: configurationPath, message: stringifyMessage(validateResult.error).trim()})
733+
}
734+
735+
return moduleInstance
736+
}
737+
652738
private async loadExtensions(appDirectory: string, appConfiguration: TConfig): Promise<ExtensionInstance[]> {
653739
if (this.specifications.length === 0) return []
654740

@@ -827,9 +913,10 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
827913
return configContent ? extensionInstance : undefined
828914
}
829915

830-
private async findEntryPath(directory: string, specification: ExtensionSpecification) {
916+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
917+
private async findEntryPath(directory: string, specification: {identifier: string; appModuleFeatures?: (...args: any[]) => string[]}) {
831918
let entryPath
832-
if (specification.appModuleFeatures().includes('single_js_entry_path')) {
919+
if (specification.appModuleFeatures?.().includes('single_js_entry_path')) {
833920
entryPath = (
834921
await Promise.all(
835922
['index']
@@ -897,6 +984,7 @@ type ConfigurationLoaderResult<
897984
TModuleSpec extends ExtensionSpecification,
898985
> = AppConfigurationInterface<TConfig, TModuleSpec> & {
899986
configurationLoadResultMetadata: ConfigurationLoadResultMetadata
987+
moduleRegistry?: ModuleRegistry
900988
}
901989

902990
/**

packages/app/src/cli/models/extensions/load-specifications.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {ExtensionSpecification} from './specification.js'
2+
import {ModuleRegistry} from './module-registry.js'
23
import appHomeSpec, {AppHomeSpecIdentifier} from './specifications/app_config_app_home.js'
34
import appProxySpec, {AppProxySpecIdentifier} from './specifications/app_config_app_proxy.js'
45
import appPOSSpec, {PosSpecIdentifier} from './specifications/app_config_point_of_sale.js'
@@ -51,6 +52,17 @@ export async function loadLocalExtensionsSpecifications(): Promise<ExtensionSpec
5152
return loadSpecifications().sort(sortConfigModules)
5253
}
5354

55+
/**
56+
* Load the module registry with all migrated module descriptors.
57+
* Descriptors registered here use the new ApplicationModule subclass path
58+
* instead of the legacy ExtensionSpecification factory pattern.
59+
*/
60+
export function loadModuleRegistry(): ModuleRegistry {
61+
const registry = new ModuleRegistry()
62+
// Descriptors will be registered here as specs are migrated.
63+
return registry
64+
}
65+
5466
function loadSpecifications() {
5567
const configModuleSpecs = [
5668
appAccessSpec,

packages/app/src/cli/services/generate/fetch-extension-specifications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export async function fetchSpecifications({
6060
return [...updatedSpecs]
6161
}
6262

63+
6364
async function mergeLocalAndRemoteSpecs(
6465
local: ExtensionSpecification[],
6566
remote: RemoteSpecification[],

0 commit comments

Comments
 (0)