@@ -21,6 +21,8 @@ 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'
25+ import { loadModuleRegistry } from '../extensions/load-specifications.js'
2426import { ExtensionsArraySchema , UnifiedSchema } from '../extensions/schemas.js'
2527import { ExtensionSpecification , isAppConfigSpecification } from '../extensions/specification.js'
2628import { CreateAppOptions , Flag } from '../../utilities/developer-platform-client.js'
@@ -320,6 +322,23 @@ export async function loadAppFromContext<TModuleSpec extends ExtensionSpecificat
320322 usesCliManagedUrls : configuration . build ?. automatically_update_urls_on_dev ,
321323 }
322324
325+ // Build the module registry and merge remote spec values into it so
326+ // descriptors have up-to-date identity data from the platform API.
327+ const moduleRegistry = loadModuleRegistry ( )
328+ const remoteSpecsForMerge = specifications . map ( ( spec ) => ( {
329+ name : spec . externalName ,
330+ externalName : spec . externalName ,
331+ identifier : spec . identifier ,
332+ gated : false ,
333+ externalIdentifier : spec . externalIdentifier ,
334+ experience : spec . experience as 'extension' | 'configuration' | 'deprecated' ,
335+ managementExperience : 'cli' as const ,
336+ registrationLimit : spec . registrationLimit ,
337+ uidStrategy : spec . uidStrategy ,
338+ surface : spec . surface ,
339+ } ) )
340+ moduleRegistry . mergeRemoteSpecs ( remoteSpecsForMerge )
341+
323342 const loadedConfiguration : ConfigurationLoaderResult < CurrentAppConfiguration , TModuleSpec > = {
324343 directory : project . directory ,
325344 configPath : configurationPath ,
@@ -328,6 +347,7 @@ export async function loadAppFromContext<TModuleSpec extends ExtensionSpecificat
328347 configSchema,
329348 specifications,
330349 remoteFlags,
350+ moduleRegistry,
331351 }
332352
333353 const loader = new AppLoader < CurrentAppConfiguration , TModuleSpec > ( {
@@ -459,6 +479,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
459479 private readonly loadedConfiguration : ConfigurationLoaderResult < TConfig , TModuleSpec >
460480 private readonly reloadState : ReloadState | undefined
461481 private readonly project : Project
482+ private readonly moduleRegistry : ModuleRegistry
462483
463484 constructor ( {
464485 ignoreUnknownExtensions,
@@ -472,6 +493,7 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
472493 this . loadedConfiguration = loadedConfiguration
473494 this . reloadState = reloadState
474495 this . project = project
496+ this . moduleRegistry = loadedConfiguration . moduleRegistry ?? new ModuleRegistry ( )
475497 }
476498
477499 private get activeConfigFile ( ) : TomlFile | undefined {
@@ -587,6 +609,14 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
587609 configurationPath : string ,
588610 directory : string ,
589611 ) : Promise < ExtensionInstance | undefined > {
612+ // Check the module registry first (new subclass-based path).
613+ // If a descriptor is found, use its factory to create the right subclass.
614+ const descriptor = this . moduleRegistry . findForType ( type )
615+ if ( descriptor ) {
616+ return this . createModuleFromDescriptor ( descriptor , configurationObject , configurationPath , directory )
617+ }
618+
619+ // Fall back to legacy ExtensionSpecification path.
590620 const specification = this . findSpecificationForType ( type )
591621 let entryPath
592622 let usedKnownSpecification = false
@@ -642,6 +672,62 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
642672 return extensionInstance
643673 }
644674
675+ private async createModuleFromDescriptor (
676+ descriptor : ReturnType < ModuleRegistry [ 'findForType' ] > & { } ,
677+ configurationObject : object ,
678+ configurationPath : string ,
679+ directory : string ,
680+ ) : Promise < ExtensionInstance | undefined > {
681+ const parseResult = descriptor . parseConfigurationObject ( configurationObject )
682+ if ( parseResult . state === 'error' ) {
683+ if ( parseResult . errors ) {
684+ for ( const error of parseResult . errors ) {
685+ this . errors . addError ( {
686+ file : configurationPath ,
687+ message : error . message ?? `Validation error at ${ error . path . join ( '.' ) } ` ,
688+ } )
689+ }
690+ }
691+ return undefined
692+ }
693+
694+ const configuration = parseResult . data
695+ const entryPath = await this . findEntryPath ( directory , descriptor )
696+
697+ const moduleInstance = descriptor . createModule ( {
698+ configuration,
699+ configurationPath,
700+ entryPath,
701+ directory,
702+ remoteSpec : {
703+ name : descriptor . externalName ,
704+ externalName : descriptor . externalName ,
705+ identifier : descriptor . identifier ,
706+ gated : false ,
707+ externalIdentifier : descriptor . externalIdentifier ,
708+ experience : descriptor . experience ,
709+ managementExperience : 'cli' ,
710+ registrationLimit : descriptor . registrationLimit ,
711+ uidStrategy : descriptor . uidStrategy ,
712+ surface : descriptor . surface ,
713+ } ,
714+ } )
715+
716+ if ( this . reloadState && configuration . handle ) {
717+ const previousDevUUID = this . reloadState . extensionDevUUIDs . get ( configuration . handle )
718+ if ( previousDevUUID ) {
719+ moduleInstance . devUUID = previousDevUUID
720+ }
721+ }
722+
723+ const validateResult = await moduleInstance . validate ( )
724+ if ( validateResult . isErr ( ) ) {
725+ this . errors . addError ( { file : configurationPath , message : stringifyMessage ( validateResult . error ) . trim ( ) } )
726+ }
727+
728+ return moduleInstance
729+ }
730+
645731 private async loadExtensions ( appDirectory : string , appConfiguration : TConfig ) : Promise < ExtensionInstance [ ] > {
646732 if ( this . specifications . length === 0 ) return [ ]
647733
@@ -820,9 +906,10 @@ class AppLoader<TConfig extends CurrentAppConfiguration, TModuleSpec extends Ext
820906 return configContent ? extensionInstance : undefined
821907 }
822908
823- private async findEntryPath ( directory : string , specification : ExtensionSpecification ) {
909+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
910+ private async findEntryPath ( directory : string , specification : { identifier : string ; appModuleFeatures ?: ( ...args : any [ ] ) => string [ ] } ) {
824911 let entryPath
825- if ( specification . appModuleFeatures ( ) . includes ( 'single_js_entry_path' ) ) {
912+ if ( specification . appModuleFeatures ?. ( ) . includes ( 'single_js_entry_path' ) ) {
826913 entryPath = (
827914 await Promise . all (
828915 [ 'index' ]
@@ -890,6 +977,7 @@ type ConfigurationLoaderResult<
890977 TModuleSpec extends ExtensionSpecification ,
891978> = AppConfigurationInterface < TConfig , TModuleSpec > & {
892979 configurationLoadResultMetadata : ConfigurationLoadResultMetadata
980+ moduleRegistry ?: ModuleRegistry
893981}
894982
895983/**
0 commit comments