@@ -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'
@@ -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/**
0 commit comments