feat: Merge plugin modules (#10895)
Fixes: FRMW-2858
This PR merge the modules exported by the plugins with the modules defined within the user config. As a result, all modules get loaded without changing the internals of the loader.
However, you cannot disable the module of a plugin by re-adding it to the `modules` array. That is something we should handle separately.
We've added the breaking change label because of the following fix:
We did broke the ability to completely disable modules in the past pr's, in this pr we re add the ability to disable a module and that this modules does not get loaded at all. ([here](6dd164f783))
Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
@@ -106,7 +106,8 @@ export async function loadModules(args: {
|
||||
let declaration: any = {}
|
||||
let definition: Partial<ModuleDefinition> | undefined = undefined
|
||||
|
||||
if (mod === false) {
|
||||
// TODO: We are keeping mod === false for backward compatibility for now
|
||||
if (mod === false || (isObject(mod) && "disable" in mod && mod.disable)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -944,6 +944,13 @@ type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & {
|
||||
disable?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Modules accepted by the defineConfig function
|
||||
*/
|
||||
export type InputConfigModules = Partial<
|
||||
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
|
||||
>[]
|
||||
|
||||
/**
|
||||
* The configuration accepted by the "defineConfig" helper
|
||||
*/
|
||||
@@ -951,9 +958,7 @@ export type InputConfig = Partial<
|
||||
Omit<ConfigModule, "admin" | "modules"> & {
|
||||
admin: Partial<ConfigModule["admin"]>
|
||||
modules:
|
||||
| Partial<
|
||||
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
|
||||
>[]
|
||||
| InputConfigModules
|
||||
/**
|
||||
* @deprecated use the array instead
|
||||
*/
|
||||
@@ -967,5 +972,5 @@ export type PluginDetails = {
|
||||
id: string
|
||||
options: Record<string, unknown>
|
||||
version: string
|
||||
modules?: InputConfig["modules"]
|
||||
modules?: InputConfigModules
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ describe("defineConfig", function () {
|
||||
`)
|
||||
})
|
||||
|
||||
it("should not include disabled modules", function () {
|
||||
it("should include disabled modules", function () {
|
||||
expect(
|
||||
defineConfig({
|
||||
projectConfig: {
|
||||
@@ -837,6 +837,9 @@ describe("defineConfig", function () {
|
||||
"cache": {
|
||||
"resolve": "@medusajs/medusa/cache-inmemory",
|
||||
},
|
||||
"cart": {
|
||||
"disable": true,
|
||||
},
|
||||
"currency": {
|
||||
"resolve": "@medusajs/medusa/currency",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { MODULE_PACKAGE_NAMES, Modules } from "../../modules-sdk"
|
||||
import { transformModules } from "../define-config"
|
||||
|
||||
describe("transformModules", () => {
|
||||
test("convert array of modules to an object", () => {
|
||||
const modules = transformModules([
|
||||
{
|
||||
resolve: require.resolve("../__fixtures__/define-config/github"),
|
||||
options: {
|
||||
apiKey: "test",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(modules).toEqual({
|
||||
GithubModuleService: {
|
||||
options: {
|
||||
apiKey: "test",
|
||||
},
|
||||
resolve: require.resolve("../__fixtures__/define-config/github"),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("transform default module", () => {
|
||||
const modules = transformModules([
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
},
|
||||
])
|
||||
|
||||
expect(modules).toEqual({
|
||||
cache: {
|
||||
resolve: "@medusajs/medusa/cache-inmemory",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("should manage loading priority of modules when its disabled at a later stage in the array", () => {
|
||||
const modules = transformModules([
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
},
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
disable: true,
|
||||
},
|
||||
])
|
||||
|
||||
expect(modules).toEqual({
|
||||
cache: {
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
disable: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("should manage loading priority of modules when its not disabled at a later stage in the array", () => {
|
||||
const modules = transformModules([
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
},
|
||||
])
|
||||
|
||||
expect(modules).toEqual({
|
||||
cache: {
|
||||
resolve: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ConfigModule,
|
||||
InputConfig,
|
||||
InputConfigModules,
|
||||
InternalModuleDeclaration,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -108,14 +109,79 @@ export function defineConfig(config: InputConfig = {}): ConfigModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* The user API allow to use array of modules configuration. This method manage the loading of the user modules
|
||||
* along side the default modules and re map them to an object.
|
||||
* Transforms an array of modules into an object. The last module will
|
||||
* take precedence in case of duplicate modules
|
||||
*/
|
||||
export function transformModules(
|
||||
modules: InputConfigModules
|
||||
): Exclude<ConfigModule["modules"], undefined> {
|
||||
const remappedModules = modules.reduce((acc, moduleConfig) => {
|
||||
if (moduleConfig.scope === "external" && !moduleConfig.key) {
|
||||
throw new Error(
|
||||
"External modules configuration must have a 'key'. Please provide a key for the module."
|
||||
)
|
||||
}
|
||||
|
||||
if ("disable" in moduleConfig && "key" in moduleConfig) {
|
||||
acc[moduleConfig.key!] = moduleConfig
|
||||
}
|
||||
|
||||
// TODO: handle external modules later
|
||||
let serviceName: string =
|
||||
"key" in moduleConfig && moduleConfig.key ? moduleConfig.key : ""
|
||||
delete moduleConfig.key
|
||||
|
||||
if (!serviceName && "resolve" in moduleConfig) {
|
||||
if (
|
||||
isString(moduleConfig.resolve!) &&
|
||||
REVERSED_MODULE_PACKAGE_NAMES[moduleConfig.resolve!]
|
||||
) {
|
||||
serviceName = REVERSED_MODULE_PACKAGE_NAMES[moduleConfig.resolve!]
|
||||
acc[serviceName] = moduleConfig
|
||||
return acc
|
||||
}
|
||||
|
||||
let resolution = isString(moduleConfig.resolve!)
|
||||
? normalizeImportPathWithSource(moduleConfig.resolve as string)
|
||||
: moduleConfig.resolve
|
||||
|
||||
const moduleExport = isString(resolution)
|
||||
? require(resolution)
|
||||
: resolution
|
||||
|
||||
const defaultExport = resolveExports(moduleExport).default
|
||||
|
||||
const joinerConfig =
|
||||
typeof defaultExport.service.prototype.__joinerConfig === "function"
|
||||
? defaultExport.service.prototype.__joinerConfig() ?? {}
|
||||
: defaultExport.service.prototype.__joinerConfig ?? {}
|
||||
|
||||
serviceName = joinerConfig.serviceName
|
||||
|
||||
if (!serviceName) {
|
||||
throw new Error(
|
||||
`Module ${moduleConfig.resolve} doesn't have a serviceName. Please provide a 'key' for the module or check the service joiner config.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
acc[serviceName] = moduleConfig
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return remappedModules as Exclude<ConfigModule["modules"], undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* The user API allow to use array of modules configuration. This method manage the loading of the
|
||||
* user modules along side the default modules and re map them to an object.
|
||||
*
|
||||
* @param configModules
|
||||
*/
|
||||
function resolveModules(
|
||||
configModules: InputConfig["modules"]
|
||||
): ConfigModule["modules"] {
|
||||
): Exclude<ConfigModule["modules"], undefined> {
|
||||
/**
|
||||
* The default set of modules to always use. The end user can swap
|
||||
* the modules by providing an alternate implementation via their
|
||||
@@ -225,67 +291,5 @@ function resolveModules(
|
||||
}
|
||||
}
|
||||
|
||||
const remappedModules = modules.reduce((acc, moduleConfig) => {
|
||||
if (moduleConfig.scope === "external" && !moduleConfig.key) {
|
||||
throw new Error(
|
||||
"External modules configuration must have a 'key'. Please provide a key for the module."
|
||||
)
|
||||
}
|
||||
|
||||
if ("disable" in moduleConfig && "key" in moduleConfig) {
|
||||
acc[moduleConfig.key!] = moduleConfig
|
||||
}
|
||||
|
||||
// TODO: handle external modules later
|
||||
let serviceName: string =
|
||||
"key" in moduleConfig && moduleConfig.key ? moduleConfig.key : ""
|
||||
delete moduleConfig.key
|
||||
|
||||
if (!serviceName && "resolve" in moduleConfig) {
|
||||
if (
|
||||
isString(moduleConfig.resolve!) &&
|
||||
REVERSED_MODULE_PACKAGE_NAMES[moduleConfig.resolve!]
|
||||
) {
|
||||
serviceName = REVERSED_MODULE_PACKAGE_NAMES[moduleConfig.resolve!]
|
||||
acc[serviceName] = moduleConfig
|
||||
return acc
|
||||
}
|
||||
|
||||
let resolution = isString(moduleConfig.resolve!)
|
||||
? normalizeImportPathWithSource(moduleConfig.resolve as string)
|
||||
: moduleConfig.resolve
|
||||
|
||||
const moduleExport = isString(resolution)
|
||||
? require(resolution)
|
||||
: resolution
|
||||
|
||||
const defaultExport = resolveExports(moduleExport).default
|
||||
|
||||
const joinerConfig =
|
||||
typeof defaultExport.service.prototype.__joinerConfig === "function"
|
||||
? defaultExport.service.prototype.__joinerConfig() ?? {}
|
||||
: defaultExport.service.prototype.__joinerConfig ?? {}
|
||||
|
||||
serviceName = joinerConfig.serviceName
|
||||
|
||||
if (!serviceName) {
|
||||
throw new Error(
|
||||
`Module ${moduleConfig.resolve} doesn't have a serviceName. Please provide a 'key' for the module or check the service joiner config.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
acc[serviceName] = moduleConfig
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Remove any modules set to false
|
||||
Object.keys(remappedModules).forEach((key) => {
|
||||
if (remappedModules[key].disable) {
|
||||
delete remappedModules[key]
|
||||
}
|
||||
})
|
||||
|
||||
return remappedModules as ConfigModule["modules"]
|
||||
return transformModules(modules)
|
||||
}
|
||||
|
||||
@@ -80,3 +80,4 @@ export * from "./trim-zeros"
|
||||
export * from "./upper-case-first"
|
||||
export * from "./validate-handle"
|
||||
export * from "./wrap-handler"
|
||||
export * from "./merge-plugin-modules"
|
||||
|
||||
35
packages/core/utils/src/common/merge-plugin-modules.ts
Normal file
35
packages/core/utils/src/common/merge-plugin-modules.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type {
|
||||
PluginDetails,
|
||||
ConfigModule,
|
||||
InputConfigModules,
|
||||
} from "@medusajs/types"
|
||||
import { transformModules } from "./define-config"
|
||||
|
||||
/**
|
||||
* Mutates the configModules object and merges the plugin modules with
|
||||
* the modules defined inside the user-config file
|
||||
*/
|
||||
export function mergePluginModules(
|
||||
configModule: ConfigModule,
|
||||
plugins: PluginDetails[]
|
||||
) {
|
||||
/**
|
||||
* Create a flat array of all the modules exposed by the registered
|
||||
* plugins
|
||||
*/
|
||||
const pluginsModules = plugins.reduce((result, plugin) => {
|
||||
if (plugin.modules) {
|
||||
result = result.concat(plugin.modules)
|
||||
}
|
||||
return result
|
||||
}, [] as InputConfigModules)
|
||||
|
||||
/**
|
||||
* Merge plugin modules with the modules defined within the
|
||||
* config file.
|
||||
*/
|
||||
configModule.modules = {
|
||||
...transformModules(pluginsModules),
|
||||
...configModule.modules,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user