Skip to content

Commit 8c23aab

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 616f287 commit 8c23aab

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

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)