diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 3893787954d..df14a082d5b 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -21,6 +21,8 @@ import { import {configurationFileNames, dotEnvFileNames} from '../../constants.js' import metadata from '../../metadata.js' import {ExtensionInstance, SpecificationBackedExtension} from '../extensions/extension-instance.js' +import {ModuleRegistry} from '../extensions/module-registry.js' +import {loadModuleRegistry} from '../extensions/load-specifications.js' import {ExtensionsArraySchema, UnifiedSchema} from '../extensions/schemas.js' import {ExtensionSpecification, isAppConfigSpecification} from '../extensions/specification.js' import {CreateAppOptions, Flag} from '../../utilities/developer-platform-client.js' @@ -327,6 +329,23 @@ export async function loadAppFromContext ({ + name: spec.externalName, + externalName: spec.externalName, + identifier: spec.identifier, + gated: false, + externalIdentifier: spec.externalIdentifier, + experience: spec.experience as 'extension' | 'configuration' | 'deprecated', + managementExperience: 'cli' as const, + registrationLimit: spec.registrationLimit, + uidStrategy: spec.uidStrategy, + surface: spec.surface, + })) + moduleRegistry.mergeRemoteSpecs(remoteSpecsForMerge) + const loadedConfiguration: ConfigurationLoaderResult = { directory: project.directory, configPath: configurationPath, @@ -335,6 +354,7 @@ export async function loadAppFromContext({ @@ -466,6 +486,7 @@ class AppLoader private readonly reloadState: ReloadState | undefined private readonly project: Project + private readonly moduleRegistry: ModuleRegistry constructor({ ignoreUnknownExtensions, @@ -479,6 +500,7 @@ class AppLoader { + // Check the module registry first (new subclass-based path). + // If a descriptor is found, use its factory to create the right subclass. + const descriptor = this.moduleRegistry.findForType(type) + if (descriptor) { + return this.createModuleFromDescriptor(descriptor, configurationObject, configurationPath, directory) + } + + // Fall back to legacy ExtensionSpecification path. const specification = this.findSpecificationForType(type) let entryPath let usedKnownSpecification = false @@ -649,6 +679,62 @@ class AppLoader & {}, + configurationObject: object, + configurationPath: string, + directory: string, + ): Promise { + const parseResult = descriptor.parseConfigurationObject(configurationObject) + if (parseResult.state === 'error') { + if (parseResult.errors) { + for (const error of parseResult.errors) { + this.errors.addError({ + file: configurationPath, + message: error.message ?? `Validation error at ${error.path.join('.')}`, + }) + } + } + return undefined + } + + const configuration = parseResult.data + const entryPath = await this.findEntryPath(directory, descriptor) + + const moduleInstance = descriptor.createModule({ + configuration, + configurationPath, + entryPath, + directory, + remoteSpec: { + name: descriptor.externalName, + externalName: descriptor.externalName, + identifier: descriptor.identifier, + gated: false, + externalIdentifier: descriptor.externalIdentifier, + experience: descriptor.experience, + managementExperience: 'cli', + registrationLimit: descriptor.registrationLimit, + uidStrategy: descriptor.uidStrategy, + surface: descriptor.surface, + }, + }) + + if (this.reloadState && configuration.handle) { + const previousDevUUID = this.reloadState.extensionDevUUIDs.get(configuration.handle) + if (previousDevUUID) { + moduleInstance.devUUID = previousDevUUID + } + } + + const validateResult = await moduleInstance.validate() + if (validateResult.isErr()) { + this.errors.addError({file: configurationPath, message: stringifyMessage(validateResult.error).trim()}) + } + + return moduleInstance + } + private async loadExtensions(appDirectory: string, appConfiguration: TConfig): Promise { if (this.specifications.length === 0) return [] @@ -827,9 +913,10 @@ class AppLoader string[]}) { let entryPath - if (specification.appModuleFeatures().includes('single_js_entry_path')) { + if (specification.appModuleFeatures?.().includes('single_js_entry_path')) { entryPath = ( await Promise.all( ['index'] @@ -897,6 +984,7 @@ type ConfigurationLoaderResult< TModuleSpec extends ExtensionSpecification, > = AppConfigurationInterface & { configurationLoadResultMetadata: ConfigurationLoadResultMetadata + moduleRegistry?: ModuleRegistry } /** diff --git a/packages/app/src/cli/models/extensions/load-specifications.ts b/packages/app/src/cli/models/extensions/load-specifications.ts index b2f4c7d3a2f..0a6cdd29f8d 100644 --- a/packages/app/src/cli/models/extensions/load-specifications.ts +++ b/packages/app/src/cli/models/extensions/load-specifications.ts @@ -1,4 +1,5 @@ import {ExtensionSpecification} from './specification.js' +import {ModuleRegistry} from './module-registry.js' import appHomeSpec, {AppHomeSpecIdentifier} from './specifications/app_config_app_home.js' import appProxySpec, {AppProxySpecIdentifier} from './specifications/app_config_app_proxy.js' import appPOSSpec, {PosSpecIdentifier} from './specifications/app_config_point_of_sale.js' @@ -51,6 +52,17 @@ export async function loadLocalExtensionsSpecifications(): Promise