diff --git a/.changeset/bright-steaks-cheer.md b/.changeset/bright-steaks-cheer.md new file mode 100644 index 0000000000..9cc7e5a8ca --- /dev/null +++ b/.changeset/bright-steaks-cheer.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa": patch +"@medusajs/modules-sdk": patch +"@medusajs/types": patch +--- + +chore(medusa, modules-sdk, types): Refactor modules loading from medusa diff --git a/packages/medusa/src/helpers/test-request.js b/packages/medusa/src/helpers/test-request.js index 2e50f54626..4f25180437 100644 --- a/packages/medusa/src/helpers/test-request.js +++ b/packages/medusa/src/helpers/test-request.js @@ -1,6 +1,8 @@ import { moduleHelper, moduleLoader, + ModulesDefinition, + registerMedusaModule, registerModules, } from "@medusajs/modules-sdk" import { asValue, createContainer } from "awilix" @@ -17,6 +19,7 @@ import passportLoader from "../loaders/passport" import repositories from "../loaders/repositories" import servicesLoader from "../loaders/services" import strategiesLoader from "../loaders/strategies" +import modules from "../modules-config" const adminSessionOpts = { cookieName: "session", @@ -31,6 +34,14 @@ const clientSessionOpts = { } const moduleResolutions = registerModules({}) +// Load non legacy modules +Object.keys(modules).map((moduleKey) => { + moduleResolutions[moduleKey] = registerMedusaModule( + moduleKey, + ModulesDefinition[moduleKey] + )[moduleKey] +}) + const config = { projectConfig: { jwt_secret: "supersecret", @@ -91,24 +102,37 @@ testApp.use((req, res, next) => { next() }) -featureFlagLoader(config) -models({ container, configModule: config, isTest: true }) -repositories({ container, isTest: true }) -servicesLoader({ container, configModule: config }) -strategiesLoader({ container, configModule: config }) -passportLoader({ app: testApp, container, configModule: config }) -moduleLoader({ container, moduleResolutions }) - -testApp.use((req, res, next) => { - req.scope = container.createScope() - next() +let supertestRequest +let resolveIsInit +const isInit = new Promise((resolve) => { + resolveIsInit = resolve }) -apiLoader({ container, app: testApp, configModule: config }) +async function init() { + featureFlagLoader(config) + models({ container, configModule: config, isTest: true }) + repositories({ container, isTest: true }) + servicesLoader({ container, configModule: config }) + strategiesLoader({ container, configModule: config }) + await passportLoader({ app: testApp, container, configModule: config }) + await moduleLoader({ container, moduleResolutions }) -const supertestRequest = supertest(testApp) + testApp.use((req, res, next) => { + req.scope = container.createScope() + next() + }) + + await apiLoader({ container, app: testApp, configModule: config }) + + supertestRequest = supertest(testApp) + resolveIsInit(true) +} + +init() export async function request(method, url, opts = {}) { + await isInit + const { payload, query, headers = {}, flags = [] } = opts flags.forEach((flag) => { diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 95205dbefe..223a012271 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,4 +1,11 @@ -import { MedusaApp, moduleLoader, registerModules } from "@medusajs/modules-sdk" +import { + ExternalModuleDeclaration, + InternalModuleDeclaration, + MedusaApp, + moduleLoader, + ModulesDefinition, + registerModules, +} from "@medusajs/modules-sdk" import { ContainerRegistrationKeys } from "@medusajs/utils" import { asValue } from "awilix" import { Express, NextFunction, Request, Response } from "express" @@ -11,7 +18,7 @@ import { Connection } from "typeorm" import { joinerConfig } from "../joiner-config" import modulesConfig from "../modules-config" import { MedusaContainer } from "../types/global" -import { remoteQueryFetchData } from "../utils" +import { isObject, remoteQueryFetchData } from "../utils" import apiLoader from "./api" import loadConfig from "./config" import databaseLoader, { dataSource } from "./database" @@ -30,6 +37,7 @@ import searchIndexLoader from "./search-index" import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" +import { ConfigModule } from "@medusajs/types" type Options = { directory: string @@ -37,6 +45,34 @@ type Options = { isTest: boolean } +/** + * Merge the modules config from the medusa-config file with the modules config from medusa package + * @param modules + * @param medusaInternalModulesConfig + */ +function mergeModulesConfig( + modules: ConfigModule["modules"], + medusaInternalModulesConfig +) { + for (const [moduleName, moduleConfig] of Object.entries(modules as any)) { + const moduleDefinition = ModulesDefinition[moduleName] + + if (moduleDefinition?.isLegacy) { + continue + } + + const isModuleEnabled = moduleConfig === true || isObject(moduleConfig) + + if (!isModuleEnabled) { + delete medusaInternalModulesConfig[moduleName] + } else { + medusaInternalModulesConfig[moduleName] = moduleConfig as Partial< + InternalModuleDeclaration | ExternalModuleDeclaration + > + } + } +} + export default async ({ directory: rootDirectory, expressApp, @@ -98,10 +134,15 @@ export default async ({ await pgConnectionLoader({ container, configModule }) const modulesActivity = Logger.activity(`Initializing modules${EOL}`) + track("MODULES_INIT_STARTED") await moduleLoader({ container, - moduleResolutions: registerModules(configModule?.modules), + moduleResolutions: registerModules(configModule?.modules, { + loadLegacyOnly: featureFlagRouter.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ), + }), logger: Logger, }) const modAct = Logger.success(modulesActivity, "Modules initialized") || {} @@ -185,7 +226,10 @@ export default async ({ Logger.success(searchActivity, "Indexing event emitted") || {} track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) + // Only load non legacy modules, the legacy modules (non migrated yet) are retrieved by the registerModule above if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) { + mergeModulesConfig(configModule.modules ?? {}, modulesConfig) + const { query } = await MedusaApp({ modulesConfig, servicesConfig: joinerConfig, diff --git a/packages/medusa/src/modules-config.ts b/packages/medusa/src/modules-config.ts index b9ca369ee5..59c6f03619 100644 --- a/packages/medusa/src/modules-config.ts +++ b/packages/medusa/src/modules-config.ts @@ -1,9 +1,7 @@ import { MedusaModuleConfig, Modules } from "@medusajs/modules-sdk" -const modules: MedusaModuleConfig = [ - { - module: Modules.PRODUCT, - }, -] +const modules: MedusaModuleConfig = { + [Modules.PRODUCT]: true, +} export default modules diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 3db4673ce6..78b00d0e9c 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -35,6 +35,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = { [Modules.EVENT_BUS]: { key: Modules.EVENT_BUS, + isLegacy: true, registrationName: ModuleRegistrationName.EVENT_BUS, defaultPackage: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS], label: "EventBusModuleService", @@ -48,6 +49,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = }, [Modules.STOCK_LOCATION]: { key: Modules.STOCK_LOCATION, + isLegacy: true, registrationName: ModuleRegistrationName.STOCK_LOCATION, defaultPackage: false, label: "StockLocationService", @@ -62,6 +64,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = }, [Modules.INVENTORY]: { key: Modules.INVENTORY, + isLegacy: true, registrationName: ModuleRegistrationName.INVENTORY, defaultPackage: false, label: "InventoryService", @@ -76,6 +79,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = }, [Modules.CACHE]: { key: Modules.CACHE, + isLegacy: true, registrationName: ModuleRegistrationName.CACHE, defaultPackage: MODULE_PACKAGE_NAMES[Modules.CACHE], label: "CacheService", diff --git a/packages/modules-sdk/src/loaders/__tests__/register-modules.ts b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts index d1e20fb683..1b71534323 100644 --- a/packages/modules-sdk/src/loaders/__tests__/register-modules.ts +++ b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts @@ -16,6 +16,7 @@ describe("module definitions loader", () => { registrationName: "testService", defaultPackage: "@medusajs/test-service", label: "TestService", + isLegacy: true, isRequired: false, canOverride: true, defaultModuleDeclaration: { diff --git a/packages/modules-sdk/src/loaders/register-modules.ts b/packages/modules-sdk/src/loaders/register-modules.ts index 9cc2ffd75d..e4a46d4052 100644 --- a/packages/modules-sdk/src/loaders/register-modules.ts +++ b/packages/modules-sdk/src/loaders/register-modules.ts @@ -11,18 +11,29 @@ import { isObject } from "@medusajs/utils" import resolveCwd from "resolve-cwd" import { MODULE_DEFINITIONS, ModulesDefinition } from "../definitions" +/** + * + * @param modules + * @param isolatedModules Will be removed once the isolated flag is being removed + */ export const registerModules = ( modules?: Record< string, | false | string | Partial - > + >, + { loadLegacyOnly } = { loadLegacyOnly: false } ): Record => { const moduleResolutions = {} as Record const projectModules = modules ?? {} for (const definition of MODULE_DEFINITIONS) { + // Skip non legacy modules + if (loadLegacyOnly && !definition.isLegacy) { + continue + } + const customConfig = projectModules[definition.key] const canSkip = @@ -62,6 +73,12 @@ export const registerMedusaModule = ( throw new Error(`Module: ${moduleKey} is not defined.`) } + if (modDefinition.isLegacy) { + throw new Error( + `Module: ${moduleKey} is a legacy module. Please use registerModules instead.` + ) + } + if ( isObject(moduleDeclaration) && moduleDeclaration?.scope === MODULE_SCOPE.EXTERNAL diff --git a/packages/modules-sdk/src/medusa-app.ts b/packages/modules-sdk/src/medusa-app.ts index 3af54e22ce..9a2aa8226b 100644 --- a/packages/modules-sdk/src/medusa-app.ts +++ b/packages/modules-sdk/src/medusa-app.ts @@ -1,9 +1,11 @@ import { RemoteFetchDataCallback } from "@medusajs/orchestration" import { + ExternalModuleDeclaration, + InternalModuleDeclaration, LoadedModule, MODULE_RESOURCE_TYPE, MODULE_SCOPE, - ModuleConfig, + ModuleDefinition, ModuleJoinerConfig, ModuleServiceInitializeOptions, RemoteJoinerQuery, @@ -18,13 +20,30 @@ import { MedusaModule } from "./medusa-module" import { RemoteLink } from "./remote-link" import { RemoteQuery } from "./remote-query" -export type MedusaModuleConfig = (Partial | Modules)[] -type SharedResources = { - database?: ModuleServiceInitializeOptions["database"] +export type MedusaModuleConfig = { + [key: string | Modules]: + | Partial + | true } -const isModuleConfig = (obj: any): obj is ModuleConfig => { - return isObject(obj) +export type SharedResources = { + database?: ModuleServiceInitializeOptions["database"] & { + /** + * { + * name?: string + * afterCreate?: Function + * min?: number + * max?: number + * refreshIdle?: boolean + * idleTimeoutMillis?: number + * reapIntervalMillis?: number + * returnToHead?: boolean + * priorityRange?: number + * log?: (message: string, logLevel: string) => void + * } + */ + pool?: Record + } } export async function MedusaApp({ @@ -83,20 +102,15 @@ export async function MedusaApp({ const allModules: Record = {} await Promise.all( - modules.map(async (mod: Partial | Modules) => { - let key: Modules | string = mod as Modules + Object.keys(modules).map(async (moduleName) => { + const mod = modules[moduleName] as MedusaModuleConfig + let path: string let declaration: any = {} - if (isModuleConfig(mod)) { - if (!mod.module) { - throw new Error( - `Module ${JSON.stringify(mod)} is missing module name.` - ) - } - - key = mod.module - path = mod.path ?? MODULE_PACKAGE_NAMES[key] + if (isObject(mod)) { + const mod_ = mod as unknown as InternalModuleDeclaration + path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName] declaration = { ...mod } delete declaration.definition @@ -104,10 +118,6 @@ export async function MedusaApp({ path = MODULE_PACKAGE_NAMES[mod as Modules] } - if (!path) { - throw new Error(`Module ${key} is missing path.`) - } - declaration.scope ??= MODULE_SCOPE.INTERNAL if ( @@ -118,22 +128,22 @@ export async function MedusaApp({ } const loaded = (await MedusaModule.bootstrap( - key, + moduleName, path, declaration, undefined, injectedDependencies, - isModuleConfig(mod) ? mod.definition : undefined + (isObject(mod) ? mod.definition : undefined) as ModuleDefinition )) as LoadedModule - if (allModules[key] && !Array.isArray(allModules[key])) { - allModules[key] = [] + if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) { + allModules[moduleName] = [] } - if (allModules[key]) { - ;(allModules[key] as LoadedModule[]).push(loaded[key]) + if (allModules[moduleName]) { + ;(allModules[moduleName] as LoadedModule[]).push(loaded[moduleName]) } else { - allModules[key] = loaded[key] + allModules[moduleName] = loaded[moduleName] } return loaded diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 80db4db995..86e3d8f9df 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -31,9 +31,7 @@ export type InternalModuleDeclaration = { scope: MODULE_SCOPE.INTERNAL resources: MODULE_RESOURCE_TYPE dependencies?: string[] - /** - * @deprecated The property should not be used. - */ + definition?: ModuleDefinition // That represent the definition of the module, such as the one we have for the medusa supported modules. This property is used for custom made modules. resolve?: string options?: Record /** @@ -48,6 +46,7 @@ export type InternalModuleDeclaration = { export type ExternalModuleDeclaration = { scope: MODULE_SCOPE.EXTERNAL + definition?: ModuleDefinition // That represent the definition of the module, such as the one we have for the medusa supported modules. This property is used for custom made modules. server?: { type: "http" url: string @@ -86,10 +85,8 @@ export type ModuleDefinition = { * @deprecated property will be removed in future versions */ isRequired?: boolean - /** - * If the module is queryable via Remote Joiner - */ - isQueryable?: boolean + isQueryable?: boolean // If the module is queryable via Remote Joiner + isLegacy?: boolean // If the module is a legacy module TODO: Remove once all the legacy modules are migrated dependencies?: string[] defaultModuleDeclaration: | InternalModuleDeclaration