@@ -21,6 +21,7 @@ import {
2121import { configurationFileNames , dotEnvFileNames } from '../../constants.js'
2222import metadata from '../../metadata.js'
2323import { ExtensionInstance , SpecificationBackedExtension } from '../extensions/extension-instance.js'
24+ import { ModuleRegistry } from '../extensions/module-registry.js'
2425import { ExtensionsArraySchema , UnifiedSchema } from '../extensions/schemas.js'
2526import { ExtensionSpecification , isAppConfigSpecification } from '../extensions/specification.js'
2627import { CreateAppOptions , Flag } from '../../utilities/developer-platform-client.js'
@@ -459,6 +460,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
459460 private readonly loadedConfiguration : ConfigurationLoaderResult < TConfig , TModuleSpec >
460461 private readonly reloadState : ReloadState | undefined
461462 private readonly project : Project
463+ private readonly moduleRegistry : ModuleRegistry
462464
463465 constructor ( {
464466 ignoreUnknownExtensions,
@@ -472,6 +474,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
472474 this . loadedConfiguration = loadedConfiguration
473475 this . reloadState = reloadState
474476 this . project = project
477+ this . moduleRegistry = loadedConfiguration . moduleRegistry ?? new ModuleRegistry ( )
475478 }
476479
477480 private get activeConfigFile ( ) : TomlFile | undefined {
@@ -587,6 +590,14 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
587590 configurationPath : string ,
588591 directory : string ,
589592 ) : Promise < ExtensionInstance | undefined > {
593+ // Check the module registry first (new subclass-based path).
594+ // If a descriptor is found, use its factory to create the right subclass.
595+ const descriptor = this . moduleRegistry . findForType ( type )
596+ if ( descriptor ) {
597+ return this . createModuleFromDescriptor ( descriptor , configurationObject , configurationPath , directory )
598+ }
599+
600+ // Fall back to legacy ExtensionSpecification path.
590601 const specification = this . findSpecificationForType ( type )
591602 let entryPath
592603 let usedKnownSpecification = false
@@ -642,6 +653,62 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
642653 return extensionInstance
643654 }
644655
656+ private async createModuleFromDescriptor (
657+ descriptor : ReturnType < ModuleRegistry [ 'findForType' ] > & { } ,
658+ configurationObject : object ,
659+ configurationPath : string ,
660+ directory : string ,
661+ ) : Promise < ExtensionInstance | undefined > {
662+ const parseResult = descriptor . parseConfigurationObject ( configurationObject )
663+ if ( parseResult . state === 'error' ) {
664+ if ( parseResult . errors ) {
665+ for ( const error of parseResult . errors ) {
666+ this . errors . addError ( {
667+ file : configurationPath ,
668+ message : error . message ?? `Validation error at ${ error . path . join ( '.' ) } ` ,
669+ } )
670+ }
671+ }
672+ return undefined
673+ }
674+
675+ const configuration = parseResult . data
676+ const entryPath = await this . findEntryPath ( directory , descriptor )
677+
678+ const moduleInstance = descriptor . createModule ( {
679+ configuration,
680+ configurationPath,
681+ entryPath,
682+ directory,
683+ remoteSpec : {
684+ name : descriptor . externalName ,
685+ externalName : descriptor . externalName ,
686+ identifier : descriptor . identifier ,
687+ gated : false ,
688+ externalIdentifier : descriptor . externalIdentifier ,
689+ experience : descriptor . experience ,
690+ managementExperience : 'cli' ,
691+ registrationLimit : descriptor . registrationLimit ,
692+ uidStrategy : descriptor . uidStrategy ,
693+ surface : descriptor . surface ,
694+ } ,
695+ } )
696+
697+ if ( this . reloadState && configuration . handle ) {
698+ const previousDevUUID = this . reloadState . extensionDevUUIDs . get ( configuration . handle )
699+ if ( previousDevUUID ) {
700+ moduleInstance . devUUID = previousDevUUID
701+ }
702+ }
703+
704+ const validateResult = await moduleInstance . validate ( )
705+ if ( validateResult . isErr ( ) ) {
706+ this . errors . addError ( { file : configurationPath , message : stringifyMessage ( validateResult . error ) . trim ( ) } )
707+ }
708+
709+ return moduleInstance
710+ }
711+
645712 private async loadExtensions ( appDirectory : string , appConfiguration : TConfig ) : Promise < ExtensionInstance [ ] > {
646713 if ( this . specifications . length === 0 ) return [ ]
647714
@@ -820,9 +887,10 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
820887 return configContent ? extensionInstance : undefined
821888 }
822889
823- private async findEntryPath ( directory : string , specification : ExtensionSpecification ) {
890+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
891+ private async findEntryPath ( directory : string , specification : { identifier : string ; appModuleFeatures ?: ( ...args : any [ ] ) => string [ ] } ) {
824892 let entryPath
825- if ( specification . appModuleFeatures ( ) . includes ( 'single_js_entry_path' ) ) {
893+ if ( specification . appModuleFeatures ?. ( ) . includes ( 'single_js_entry_path' ) ) {
826894 entryPath = (
827895 await Promise . all (
828896 [ 'index' ]
@@ -890,6 +958,7 @@ type ConfigurationLoaderResult<
890958 TModuleSpec extends ExtensionSpecification ,
891959> = AppConfigurationInterface < TConfig , TModuleSpec > & {
892960 configurationLoadResultMetadata : ConfigurationLoadResultMetadata
961+ moduleRegistry ?: ModuleRegistry
893962}
894963
895964/**
0 commit comments