Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Merge plugin modules #10895

Merged
merged 12 commits into from
Jan 10, 2025
7 changes: 7 additions & 0 deletions .changeset/thin-games-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---

Feat/merge plugin modules
13 changes: 9 additions & 4 deletions packages/core/types/src/common/config-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,16 +944,21 @@ type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & {
disable?: boolean
}

/**
* Modules accepted by the defineConfig function
*/
export type InputConfigModules = Partial<
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
>[]

/**
* The configuration accepted by the "defineConfig" helper
*/
export type InputConfig = Partial<
Omit<ConfigModule, "admin" | "modules"> & {
admin: Partial<ConfigModule["admin"]>
modules:
| Partial<
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
>[]
| InputConfigModules
/**
* @deprecated use the array instead
*/
Expand All @@ -967,5 +972,5 @@ export type PluginDetails = {
id: string
options: Record<string, unknown>
version: string
modules?: InputConfig["modules"]
modules?: InputConfigModules
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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("remove module 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({})
adrien2p marked this conversation as resolved.
Show resolved Hide resolved
})
})
143 changes: 77 additions & 66 deletions packages/core/utils/src/common/define-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ConfigModule,
InputConfig,
InputConfigModules,
InternalModuleDeclaration,
} from "@medusajs/types"
import {
Expand Down Expand Up @@ -108,14 +109,86 @@ 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
}, {})

// Remove any modules set to false
Object.keys(remappedModules).forEach((key) => {
adrien2p marked this conversation as resolved.
Show resolved Hide resolved
if (remappedModules[key].disable) {
delete remappedModules[key]
}
})

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
Expand Down Expand Up @@ -225,67 +298,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)
}
1 change: 1 addition & 0 deletions packages/core/utils/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 35 additions & 0 deletions packages/core/utils/src/common/merge-plugin-modules.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 3 additions & 0 deletions packages/medusa/src/commands/db/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "path"
import {
ContainerRegistrationKeys,
MedusaError,
mergePluginModules,
} from "@medusajs/framework/utils"
import { LinkLoader } from "@medusajs/framework/links"
import { logger } from "@medusajs/framework/logger"
Expand All @@ -27,6 +28,8 @@ const main = async function ({ directory, modules }) {
)

const plugins = await getResolvedPlugins(directory, configModule, true)
mergePluginModules(configModule, plugins)

const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links")
)
Expand Down
7 changes: 6 additions & 1 deletion packages/medusa/src/commands/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { join } from "path"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import {
ContainerRegistrationKeys,
mergePluginModules,
} from "@medusajs/framework/utils"
import { LinkLoader } from "@medusajs/framework/links"
import { logger } from "@medusajs/framework/logger"
import { MedusaAppLoader } from "@medusajs/framework"
Expand Down Expand Up @@ -38,6 +41,8 @@ export async function migrate({
)

const plugins = await getResolvedPlugins(directory, configModule, true)
mergePluginModules(configModule, plugins)

const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links")
)
Expand Down
3 changes: 3 additions & 0 deletions packages/medusa/src/commands/db/rollback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "path"
import {
ContainerRegistrationKeys,
MedusaError,
mergePluginModules,
} from "@medusajs/framework/utils"
import { LinkLoader } from "@medusajs/framework/links"
import { logger } from "@medusajs/framework/logger"
Expand All @@ -27,6 +28,8 @@ const main = async function ({ directory, modules }) {
)

const plugins = await getResolvedPlugins(directory, configModule, true)
mergePluginModules(configModule, plugins)

const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links")
)
Expand Down
7 changes: 6 additions & 1 deletion packages/medusa/src/commands/db/sync-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import boxen from "boxen"
import chalk from "chalk"
import { join } from "path"
import checkbox from "@inquirer/checkbox"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import {
ContainerRegistrationKeys,
mergePluginModules,
} from "@medusajs/framework/utils"
import { LinkMigrationsPlannerAction } from "@medusajs/framework/types"
import { LinkLoader } from "@medusajs/framework/links"
import { logger } from "@medusajs/framework/logger"
Expand Down Expand Up @@ -188,6 +191,8 @@ const main = async function ({ directory, executeSafe, executeAll }) {
const medusaAppLoader = new MedusaAppLoader()

const plugins = await getResolvedPlugins(directory, configModule, true)
mergePluginModules(configModule, plugins)

const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links")
)
Expand Down
Loading
Loading