diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index d6d6a65445..0b794ccab7 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -32,6 +32,7 @@ import { MODULE_PACKAGE_NAMES } from "./definitions" import { MedusaModule, MigrationOptions, + ModuleBootstrapOptions, RegisterModuleJoinerConfig, } from "./medusa-module" import { RemoteLink } from "./remote-link" @@ -97,89 +98,104 @@ export async function loadModules(args: { sharedResourcesConfig, migrationOnly = false, loaderOnly = false, - workerMode = "server", + workerMode = "server" as ModuleBootstrapOptions["workerMode"], } = args - const allModules = {} + const allModules = {} as any - await Promise.all( - Object.keys(modulesConfig).map(async (moduleName) => { - const mod = modulesConfig[moduleName] - let path: string - let moduleExports: ModuleExports | undefined = undefined - let declaration: any = {} - let definition: Partial | undefined = undefined + const modulesToLoad: { + moduleKey: string + defaultPath: string + declaration: InternalModuleDeclaration | ExternalModuleDeclaration + sharedContainer: MedusaContainer + moduleDefinition: ModuleDefinition + moduleExports?: ModuleExports + }[] = [] - // Skip disabled modules - if (mod === false) { - return + for (const moduleName of Object.keys(modulesConfig)) { + const mod = modulesConfig[moduleName] + let path: string + let moduleExports: ModuleExports | undefined = undefined + let declaration: any = {} + let definition: Partial | undefined = undefined + + if (mod === false) { + continue + } + + if (isObject(mod)) { + const mod_ = mod as unknown as InternalModuleDeclaration + path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName] + definition = mod_.definition + moduleExports = !isString(mod_.resolve) + ? (mod_.resolve as ModuleExports) + : undefined + declaration = { ...mod } + delete declaration.definition + } else { + path = MODULE_PACKAGE_NAMES[moduleName] + } + + declaration.scope ??= MODULE_SCOPE.INTERNAL + if (declaration.scope === MODULE_SCOPE.INTERNAL && !declaration.resources) { + declaration.resources = MODULE_RESOURCE_TYPE.SHARED + } + + if ( + declaration.scope === MODULE_SCOPE.INTERNAL && + declaration.resources === MODULE_RESOURCE_TYPE.SHARED + ) { + declaration.options ??= {} + declaration.options.database ??= { + ...sharedResourcesConfig?.database, } + declaration.options.database.debug ??= + sharedResourcesConfig?.database?.debug + } - if (isObject(mod)) { - const mod_ = mod as unknown as InternalModuleDeclaration - path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName] - definition = mod_.definition - moduleExports = !isString(mod_.resolve) - ? (mod_.resolve as ModuleExports) - : undefined - declaration = { ...mod } - delete declaration.definition - } else { - path = MODULE_PACKAGE_NAMES[moduleName] - } - - declaration.scope ??= MODULE_SCOPE.INTERNAL - if ( - declaration.scope === MODULE_SCOPE.INTERNAL && - !declaration.resources - ) { - declaration.resources = MODULE_RESOURCE_TYPE.SHARED - } - - if ( - declaration.scope === MODULE_SCOPE.INTERNAL && - declaration.resources === MODULE_RESOURCE_TYPE.SHARED - ) { - declaration.options ??= {} - declaration.options.database ??= { - ...sharedResourcesConfig?.database, - } - declaration.options.database.debug ??= - sharedResourcesConfig?.database?.debug - } - - const loaded = (await MedusaModule.bootstrap({ - moduleKey: moduleName, - defaultPath: path, - declaration, - sharedContainer, - moduleDefinition: definition as ModuleDefinition, - moduleExports, - migrationOnly, - loaderOnly, - workerMode, - })) as LoadedModule - - if (loaderOnly) { - return - } - - const service = loaded[moduleName] - sharedContainer.register({ - [service.__definition.key]: asValue(service), - }) - - if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) { - allModules[moduleName] = [] - } - - if (allModules[moduleName]) { - ;(allModules[moduleName] as LoadedModule[]).push(loaded[moduleName]) - } else { - allModules[moduleName] = loaded[moduleName] - } + modulesToLoad.push({ + moduleKey: moduleName, + defaultPath: path, + declaration, + sharedContainer, + moduleDefinition: definition as ModuleDefinition, + moduleExports, }) - ) + } + + const loaded = (await MedusaModule.bootstrapAll(modulesToLoad, { + migrationOnly, + loaderOnly, + workerMode, + })) as LoadedModule[] + + if (loaderOnly) { + return allModules + } + + for (const { moduleKey } of modulesToLoad) { + const service = loaded.find((loadedModule) => loadedModule[moduleKey])?.[ + moduleKey + ] + if (!service) { + throw new Error(`Module ${moduleKey} could not be loaded.`) + } + + sharedContainer.register({ + [service.__definition.key]: asValue(service), + }) + + if (allModules[moduleKey] && !Array.isArray(allModules[moduleKey])) { + allModules[moduleKey] = [] + } + + if (allModules[moduleKey]) { + ;(allModules[moduleKey] as LoadedModule[]).push(service) + } else { + allModules[moduleKey] = service + } + } + return allModules } @@ -374,7 +390,7 @@ async function MedusaApp_({ const allModules = await loadModules({ modulesConfig: modules, sharedContainer: sharedContainer_, - sharedResourcesConfig, + sharedResourcesConfig: { database: dbData }, migrationOnly, loaderOnly, workerMode, diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index 40b44b36c0..a23a536d98 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -273,6 +273,52 @@ class MedusaModule { MedusaModule.modules_.set(moduleKey, modules!) } + /** + * Load all modules and resolve them once they are loaded + * @param modulesOptions + * @param migrationOnly + * @param loaderOnly + * @param workerMode + */ + public static async bootstrapAll( + modulesOptions: Omit< + ModuleBootstrapOptions, + "migrationOnly" | "loaderOnly" | "workerMode" + >[], + { + migrationOnly, + loaderOnly, + workerMode, + }: { + migrationOnly?: boolean + loaderOnly?: boolean + workerMode?: ModuleBootstrapOptions["workerMode"] + } + ): Promise< + { + [key: string]: any + }[] + > { + return await MedusaModule.bootstrap_(modulesOptions, { + migrationOnly, + loaderOnly, + workerMode, + }) + } + + /** + * Load a single module and resolve it once it is loaded + * @param moduleKey + * @param defaultPath + * @param declaration + * @param moduleExports + * @param sharedContainer + * @param moduleDefinition + * @param injectedDependencies + * @param migrationOnly + * @param loaderOnly + * @param workerMode + */ public static async bootstrap({ moduleKey, defaultPath, @@ -287,96 +333,232 @@ class MedusaModule { }: ModuleBootstrapOptions): Promise<{ [key: string]: T }> { - const hashKey = simpleHash( - stringifyCircular({ moduleKey, defaultPath, declaration }) + const [service] = await MedusaModule.bootstrap_( + [ + { + moduleKey, + defaultPath, + declaration, + moduleExports, + sharedContainer, + moduleDefinition, + injectedDependencies, + }, + ], + { + migrationOnly, + loaderOnly, + workerMode, + } ) - if (!loaderOnly && MedusaModule.instances_.has(hashKey)) { - return MedusaModule.instances_.get(hashKey)! as { - [key: string]: T + return service as { + [key: string]: T + } + } + + /** + * Load all modules and then resolve them once they are loaded + * + * @param modulesOptions + * @param migrationOnly + * @param loaderOnly + * @param workerMode + * @protected + */ + protected static async bootstrap_( + modulesOptions: Omit< + ModuleBootstrapOptions, + "migrationOnly" | "loaderOnly" | "workerMode" + >[], + { + migrationOnly, + loaderOnly, + workerMode, + }: { + migrationOnly?: boolean + loaderOnly?: boolean + workerMode?: "shared" | "worker" | "server" + } + ): Promise< + { + [key: string]: T + }[] + > { + let loadedModules: { + hashKey: string + modDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration + moduleResolutions: Record + container: MedusaContainer + finishLoading: (arg: { [Key: string]: any }) => void + }[] = [] + + const services: { [Key: string]: any }[] = [] + + for (const moduleOptions of modulesOptions) { + const { + moduleKey, + defaultPath, + declaration, + moduleExports, + sharedContainer, + moduleDefinition, + injectedDependencies, + } = moduleOptions + + const hashKey = simpleHash( + stringifyCircular({ moduleKey, defaultPath, declaration }) + ) + + let finishLoading: any + let errorLoading: any + + const loadingPromise = new Promise((resolve, reject) => { + finishLoading = resolve + errorLoading = reject + }) + + if (!loaderOnly && MedusaModule.instances_.has(hashKey)) { + services.push(MedusaModule.instances_.get(hashKey)!) + continue } - } - if (!loaderOnly && MedusaModule.loading_.has(hashKey)) { - return MedusaModule.loading_.get(hashKey) - } - - let finishLoading: any - let errorLoading: any - - const loadingPromise = new Promise((resolve, reject) => { - finishLoading = resolve - errorLoading = reject - }) - - if (!loaderOnly) { - MedusaModule.loading_.set(hashKey, loadingPromise) - } - - let modDeclaration = - declaration ?? - ({} as InternalModuleDeclaration | ExternalModuleDeclaration) - - if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) { - modDeclaration = { - scope: declaration?.scope || MODULE_SCOPE.INTERNAL, - resources: declaration?.resources || MODULE_RESOURCE_TYPE.ISOLATED, - resolve: defaultPath, - options: declaration?.options ?? declaration, - dependencies: declaration?.dependencies ?? [], - alias: declaration?.alias, - main: declaration?.main, - worker_mode: workerMode, + if (!loaderOnly && MedusaModule.loading_.has(hashKey)) { + services.push(await MedusaModule.loading_.get(hashKey)) + continue } - } - // TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer) - const container = - modDeclaration.scope === MODULE_SCOPE.INTERNAL && - modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED - ? sharedContainer ?? createMedusaContainer() - : createMedusaContainer({}, sharedContainer) + if (!loaderOnly) { + MedusaModule.loading_.set(hashKey, loadingPromise) + } - if (injectedDependencies) { - for (const service in injectedDependencies) { - container.register(service, asValue(injectedDependencies[service])) - if (!container.hasRegistration(service)) { + let modDeclaration = + declaration ?? + ({} as InternalModuleDeclaration | ExternalModuleDeclaration) + + if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) { + modDeclaration = { + scope: declaration?.scope || MODULE_SCOPE.INTERNAL, + resources: + (declaration as InternalModuleDeclaration)?.resources || + MODULE_RESOURCE_TYPE.ISOLATED, + resolve: defaultPath, + options: declaration?.options ?? declaration, + dependencies: + (declaration as InternalModuleDeclaration)?.dependencies ?? [], + alias: declaration?.alias, + main: declaration?.main, + worker_mode: workerMode, + } as any + } + + // TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer) + const container = + modDeclaration.scope === MODULE_SCOPE.INTERNAL && + modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED + ? sharedContainer ?? createMedusaContainer() + : createMedusaContainer({}, sharedContainer) + + if (injectedDependencies) { + for (const service in injectedDependencies) { container.register(service, asValue(injectedDependencies[service])) + if (!container.hasRegistration(service)) { + container.register(service, asValue(injectedDependencies[service])) + } } } + + const moduleResolutions = registerMedusaModule( + moduleKey, + modDeclaration!, + moduleExports, + moduleDefinition + ) + + const logger_ = + container.resolve(ContainerRegistrationKeys.LOGGER, { + allowUnregistered: true, + }) ?? logger + + try { + await moduleLoader({ + container, + moduleResolutions, + logger: logger_, + migrationOnly, + loaderOnly, + }) + } catch (err) { + errorLoading(err) + throw err + } + + loadedModules.push({ + hashKey, + modDeclaration, + moduleResolutions, + container, + finishLoading, + }) } - const moduleResolutions = registerMedusaModule( - moduleKey, - modDeclaration!, - moduleExports, - moduleDefinition - ) + if (loaderOnly) { + loadedModules.forEach(({ finishLoading }) => finishLoading({})) + return [{}] + } + for (const { + hashKey, + modDeclaration, + moduleResolutions, + container, + finishLoading, + } of loadedModules) { + const service = await MedusaModule.resolveLoadedModule({ + hashKey, + modDeclaration, + moduleResolutions, + container, + }) + + MedusaModule.instances_.set(hashKey, service) + finishLoading(service) + MedusaModule.loading_.delete(hashKey) + services.push(service) + } + + return services + } + + /** + * Resolve all the modules once they all have been loaded through the bootstrap + * and store their references in the instances_ map and return them + * + * @param hashKey + * @param modDeclaration + * @param moduleResolutions + * @param container + * @private + */ + private static async resolveLoadedModule({ + hashKey, + modDeclaration, + moduleResolutions, + container, + }: { + hashKey: string + modDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration + moduleResolutions: Record + container: MedusaContainer + }): Promise<{ + [key: string]: any + }> { const logger_ = container.resolve(ContainerRegistrationKeys.LOGGER, { allowUnregistered: true, }) ?? logger - try { - await moduleLoader({ - container, - moduleResolutions, - logger: logger_, - migrationOnly, - loaderOnly, - }) - } catch (err) { - errorLoading(err) - throw err - } - - const services = {} - - if (loaderOnly) { - finishLoading(services) - return services - } + const services: { [key: string]: any } = {} for (const resolution of Object.values( moduleResolutions @@ -390,6 +572,7 @@ class MedusaModule { let joinerConfig!: ModuleJoinerConfig try { + // TODO: rework that to store on a separate property joinerConfig = await services[keyName].__joinerConfig?.() } catch { // noop @@ -424,10 +607,6 @@ class MedusaModule { }) } - MedusaModule.instances_.set(hashKey, services) - finishLoading(services) - MedusaModule.loading_.delete(hashKey) - return services } @@ -447,7 +626,7 @@ class MedusaModule { } if (MedusaModule.loading_.has(hashKey)) { - return MedusaModule.loading_.get(hashKey) + return await MedusaModule.loading_.get(hashKey) } let finishLoading: any