feat: remove dead code and refactor the logic of resolving plugins (#10874)

This commit is contained in:
Harminder Virk
2025-01-09 14:52:10 +05:30
committed by GitHub
parent 67782350a9
commit 28febfc643
13 changed files with 376 additions and 168 deletions
+8
View File
@@ -0,0 +1,8 @@
---
"@medusajs/medusa": patch
"@medusajs/test-utils": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat: remove dead code and refactor the logic of resolving plugins
@@ -922,10 +922,50 @@ export type ConfigModule = {
featureFlags: Record<string, boolean | string | Record<string, boolean>> featureFlags: Record<string, boolean | string | Record<string, boolean>>
} }
type InternalModuleDeclarationOverride = InternalModuleDeclaration & {
/**
* Optional key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name.
*/
key?: string
/**
* By default, modules are enabled, if provided as true, this will disable the module entirely.
*/
disable?: boolean
}
type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & {
/**
* key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name.
*/
key: string
/**
* By default, modules are enabled, if provided as true, this will disable the module entirely.
*/
disable?: boolean
}
/**
* The configuration accepted by the "defineConfig" helper
*/
export type InputConfig = Partial<
Omit<ConfigModule, "admin" | "modules"> & {
admin: Partial<ConfigModule["admin"]>
modules:
| Partial<
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
>[]
/**
* @deprecated use the array instead
*/
| ConfigModule["modules"]
}
>
export type PluginDetails = { export type PluginDetails = {
resolve: string resolve: string
name: string name: string
id: string id: string
options: Record<string, unknown> options: Record<string, unknown>
version: string version: string
modules?: InputConfig["modules"]
} }
@@ -1,6 +1,6 @@
import { import {
ConfigModule, ConfigModule,
ExternalModuleDeclaration, InputConfig,
InternalModuleDeclaration, InternalModuleDeclaration,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
@@ -29,42 +29,6 @@ export const DEFAULT_STORE_RESTRICTED_FIELDS = [
"payment_collections"*/ "payment_collections"*/
] ]
type InternalModuleDeclarationOverride = InternalModuleDeclaration & {
/**
* Optional key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name.
*/
key?: string
/**
* By default, modules are enabled, if provided as true, this will disable the module entirely.
*/
disable?: boolean
}
type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & {
/**
* key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name.
*/
key: string
/**
* By default, modules are enabled, if provided as true, this will disable the module entirely.
*/
disable?: boolean
}
type Config = Partial<
Omit<ConfigModule, "admin" | "modules"> & {
admin: Partial<ConfigModule["admin"]>
modules:
| Partial<
InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride
>[]
/**
* @deprecated use the array instead
*/
| ConfigModule["modules"]
}
>
/** /**
* The "defineConfig" helper can be used to define the configuration * The "defineConfig" helper can be used to define the configuration
* of a medusa application. * of a medusa application.
@@ -73,7 +37,7 @@ type Config = Partial<
* make an application work seamlessly, but still provide you the ability * make an application work seamlessly, but still provide you the ability
* to override configuration as needed. * to override configuration as needed.
*/ */
export function defineConfig(config: Config = {}): ConfigModule { export function defineConfig(config: InputConfig = {}): ConfigModule {
const { http, redisOptions, ...restOfProjectConfig } = const { http, redisOptions, ...restOfProjectConfig } =
config.projectConfig || {} config.projectConfig || {}
@@ -150,14 +114,14 @@ export function defineConfig(config: Config = {}): ConfigModule {
* @param configModules * @param configModules
*/ */
function resolveModules( function resolveModules(
configModules: Config["modules"] configModules: InputConfig["modules"]
): ConfigModule["modules"] { ): ConfigModule["modules"] {
/** /**
* The default set of modules to always use. The end user can swap * The default set of modules to always use. The end user can swap
* the modules by providing an alternate implementation via their * the modules by providing an alternate implementation via their
* config. But they can never remove a module from this list. * config. But they can never remove a module from this list.
*/ */
const modules: Config["modules"] = [ const modules: InputConfig["modules"] = [
{ resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] }, { resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] }, { resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] }, { resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] },
@@ -2,21 +2,51 @@ import { Dirent } from "fs"
import { readdir } from "fs/promises" import { readdir } from "fs/promises"
import { join } from "path" import { join } from "path"
export async function readDirRecursive(dir: string): Promise<Dirent[]> { const MISSING_NODE_ERRORS = ["ENOTDIR", "ENOENT"]
let allEntries: Dirent[] = []
const readRecursive = async (dir) => { export async function readDir(
dir: string,
options?: {
ignoreMissing?: boolean
}
) {
try {
const entries = await readdir(dir, { withFileTypes: true }) const entries = await readdir(dir, { withFileTypes: true })
return entries
} catch (error) {
if (options?.ignoreMissing && MISSING_NODE_ERRORS.includes(error.code)) {
return []
}
throw error
}
}
for (const entry of entries) { export async function readDirRecursive(
const fullPath = join(dir, entry.name) dir: string,
Object.defineProperty(entry, "path", { options?: {
value: dir, ignoreMissing?: boolean
}) }
allEntries.push(entry) ): Promise<Dirent[]> {
let allEntries: Dirent[] = []
const readRecursive = async (dir: string) => {
try {
const entries = await readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(dir, entry.name)
Object.defineProperty(entry, "path", {
value: dir,
})
allEntries.push(entry)
if (entry.isDirectory()) { if (entry.isDirectory()) {
await readRecursive(fullPath) await readRecursive(fullPath)
}
} }
} catch (error) {
if (options?.ignoreMissing && error.code === "ENOENT") {
return
}
throw error
} }
} }
@@ -61,7 +61,7 @@ async function loadCustomLinks(directory: string, container: MedusaContainer) {
const configModule = container.resolve( const configModule = container.resolve(
ContainerRegistrationKeys.CONFIG_MODULE ContainerRegistrationKeys.CONFIG_MODULE
) )
const plugins = getResolvedPlugins(directory, configModule, true) || [] const plugins = await getResolvedPlugins(directory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )
+1 -1
View File
@@ -40,7 +40,7 @@
"watch": "tsc --build --watch", "watch": "tsc --build --watch",
"build": "rimraf dist && tsc --build", "build": "rimraf dist && tsc --build",
"serve": "node dist/app.js", "serve": "node dist/app.js",
"test": "jest --silent --bail --maxWorkers=50% --forceExit" "test": "jest --silent=false --bail --maxWorkers=50% --forceExit"
}, },
"devDependencies": { "devDependencies": {
"@medusajs/framework": "^2.2.0", "@medusajs/framework": "^2.2.0",
+1 -1
View File
@@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) {
ContainerRegistrationKeys.CONFIG_MODULE ContainerRegistrationKeys.CONFIG_MODULE
) )
const plugins = getResolvedPlugins(directory, configModule, true) || [] const plugins = await getResolvedPlugins(directory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )
+1 -1
View File
@@ -37,7 +37,7 @@ export async function migrate({
ContainerRegistrationKeys.CONFIG_MODULE ContainerRegistrationKeys.CONFIG_MODULE
) )
const plugins = getResolvedPlugins(directory, configModule, true) || [] const plugins = await getResolvedPlugins(directory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )
+1 -1
View File
@@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) {
ContainerRegistrationKeys.CONFIG_MODULE ContainerRegistrationKeys.CONFIG_MODULE
) )
const plugins = getResolvedPlugins(directory, configModule, true) || [] const plugins = await getResolvedPlugins(directory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )
@@ -187,7 +187,7 @@ const main = async function ({ directory, executeSafe, executeAll }) {
const medusaAppLoader = new MedusaAppLoader() const medusaAppLoader = new MedusaAppLoader()
const plugins = getResolvedPlugins(directory, configModule, true) || [] const plugins = await getResolvedPlugins(directory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )
@@ -0,0 +1,211 @@
import path from "path"
import { defineConfig, FileSystem } from "@medusajs/framework/utils"
import { getResolvedPlugins } from "../helpers/resolve-plugins"
const BASE_DIR = path.join(__dirname, "sample-proj")
const fs = new FileSystem(BASE_DIR)
afterEach(async () => {
await fs.cleanup()
})
describe("getResolvedPlugins | relative paths", () => {
test("resolve configured plugins", async () => {
await fs.createJson("plugins/dummy/package.json", {
name: "my-dummy-plugin",
version: "1.0.0",
})
const plugins = await getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "./plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
expect(plugins).toEqual([
{
resolve: path.join(fs.basePath, "./plugins/dummy/build"),
name: "my-dummy-plugin",
id: "my-dummy-plugin",
options: { apiKey: "asecret" },
version: "1.0.0",
modules: [],
},
])
})
test("scan plugin modules", async () => {
await fs.createJson("plugins/dummy/package.json", {
name: "my-dummy-plugin",
version: "1.0.0",
})
await fs.create("plugins/dummy/build/modules/blog/index.js", ``)
const plugins = await getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "./plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
expect(plugins).toEqual([
{
resolve: path.join(fs.basePath, "./plugins/dummy/build"),
name: "my-dummy-plugin",
id: "my-dummy-plugin",
options: { apiKey: "asecret" },
version: "1.0.0",
modules: [
{
options: {
apiKey: "asecret",
},
resolve: "./plugins/dummy/build/modules/blog",
},
],
},
])
})
test("throw error when package.json file is missing", async () => {
const resolvePlugins = async () =>
getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "./plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
await expect(resolvePlugins()).rejects.toThrow(
`Unable to resolve plugin "./plugins/dummy". Make sure the plugin directory has a package.json file`
)
})
})
describe("getResolvedPlugins | package reference", () => {
test("resolve configured plugins", async () => {
await fs.createJson("package.json", {})
await fs.createJson("node_modules/@plugins/dummy/package.json", {
name: "my-dummy-plugin",
version: "1.0.0",
})
const plugins = await getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "@plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
expect(plugins).toEqual([
{
resolve: path.join(fs.basePath, "node_modules/@plugins/dummy/build"),
name: "my-dummy-plugin",
id: "my-dummy-plugin",
options: { apiKey: "asecret" },
version: "1.0.0",
modules: [],
},
])
})
test("scan plugin modules", async () => {
await fs.createJson("package.json", {})
await fs.createJson("node_modules/@plugins/dummy/package.json", {
name: "my-dummy-plugin",
version: "1.0.0",
})
await fs.create(
"node_modules/@plugins/dummy/build/modules/blog/index.js",
``
)
const plugins = await getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "@plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
expect(plugins).toEqual([
{
resolve: path.join(fs.basePath, "node_modules/@plugins/dummy/build"),
name: "my-dummy-plugin",
id: "my-dummy-plugin",
options: { apiKey: "asecret" },
version: "1.0.0",
modules: [
{
options: {
apiKey: "asecret",
},
resolve: "@plugins/dummy/build/modules/blog",
},
],
},
])
})
test("throw error when package.json file is missing", async () => {
const resolvePlugins = async () =>
getResolvedPlugins(
fs.basePath,
defineConfig({
plugins: [
{
resolve: "@plugins/dummy",
options: {
apiKey: "asecret",
},
},
],
}),
false
)
await expect(resolvePlugins()).rejects.toThrow(
`Unable to resolve plugin "@plugins/dummy". Make sure the plugin directory has a package.json file`
)
})
})
@@ -1,148 +1,103 @@
import path from "path"
import fs from "fs/promises"
import { isString, readDir } from "@medusajs/framework/utils"
import { ConfigModule, PluginDetails } from "@medusajs/framework/types" import { ConfigModule, PluginDetails } from "@medusajs/framework/types"
import { isString } from "@medusajs/framework/utils"
import fs from "fs"
import { sync as existsSync } from "fs-exists-cached"
import path, { isAbsolute } from "path"
const MEDUSA_APP_SOURCE_PATH = "src"
export const MEDUSA_PROJECT_NAME = "project-plugin" export const MEDUSA_PROJECT_NAME = "project-plugin"
function createPluginId(name: string): string { function createPluginId(name: string): string {
return name return name
} }
function createFileContentHash(path, files): string { function createFileContentHash(path: string, files: string): string {
return path + files return path + files
} }
function getExtensionDirectoryPath() {
return "src"
}
/** /**
* Load plugin details from a path. Return undefined if does not contains a package.json * Returns the absolute path to the package.json file for a
* @param pluginName * given plugin identifier.
* @param path
* @param includeExtensionDirectoryPath should include src | dist for the resolved details
*/ */
function loadPluginDetails({ async function resolvePluginPkgFile(
pluginName, rootDirectory: string,
resolvedPath, pluginPath: string
includeExtensionDirectoryPath, ): Promise<{ path: string; contents: any }> {
}: { try {
pluginName: string const pkgJSONPath = require.resolve(path.join(pluginPath, "package.json"), {
resolvedPath: string paths: [rootDirectory],
includeExtensionDirectoryPath?: boolean })
}) { const packageJSONContents = JSON.parse(
if (existsSync(`${resolvedPath}/package.json`)) { await fs.readFile(pkgJSONPath, "utf-8")
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
) )
const name = packageJSON.name || pluginName return { path: pkgJSONPath, contents: packageJSONContents }
} catch (error) {
const extensionDirectoryPath = getExtensionDirectoryPath() if (error.code === "MODULE_NOT_FOUND" || error.code === "ENOENT") {
const resolve = includeExtensionDirectoryPath throw new Error(
? path.join(resolvedPath, extensionDirectoryPath) `Unable to resolve plugin "${pluginPath}". Make sure the plugin directory has a package.json file`
: resolvedPath )
return {
resolve,
name,
id: createPluginId(name),
options: {},
version: packageJSON.version || createFileContentHash(path, `**`),
} }
throw error
} }
// Make package.json a requirement for local plugins too
throw new Error(`Plugin ${pluginName} requires a package.json file`)
} }
/** /**
* Finds the correct path for the plugin. If it is a local plugin it will be * Finds the correct path for the plugin. If it is a local plugin it will be
* found in the plugins folder. Otherwise we will look for the plugin in the * found in the plugins folder. Otherwise we will look for the plugin in the
* installed npm packages. * installed npm packages.
* @param {string} pluginName - the name of the plugin to find. Should match * @param {string} pluginPath - the name of the plugin to find. Should match
* the name of the folder where the plugin is contained. * the name of the folder where the plugin is contained.
* @return {object} the plugin details * @return {object} the plugin details
*/ */
function resolvePlugin(pluginName: string): { async function resolvePlugin(
resolve: string rootDirectory: string,
id: string pluginPath: string,
name: string options?: any
options: Record<string, unknown> ): Promise<PluginDetails> {
version: string const pkgJSON = await resolvePluginPkgFile(rootDirectory, pluginPath)
} { const resolvedPath = path.dirname(pkgJSON.path)
if (!isAbsolute(pluginName)) {
let resolvedPath = path.resolve(`./plugins/${pluginName}`)
const doesExistsInPlugin = existsSync(resolvedPath)
if (doesExistsInPlugin) { const name = pkgJSON.contents.name || pluginPath
return loadPluginDetails({ const srcDir = pkgJSON.contents.main
pluginName, ? path.dirname(pkgJSON.contents.main)
resolvedPath, : "build"
})
}
// Find the plugin in the file system const resolve = path.join(resolvedPath, srcDir)
resolvedPath = path.resolve(pluginName) const modules = await readDir(path.join(resolve, "modules"), {
const doesExistsInFileSystem = existsSync(resolvedPath) ignoreMissing: true,
})
const pluginOptions = options ?? {}
if (doesExistsInFileSystem) { return {
return loadPluginDetails({ resolve,
pluginName, name,
resolvedPath, id: createPluginId(name),
includeExtensionDirectoryPath: true, options: pluginOptions,
}) version: pkgJSON.contents.version || "0.0.0",
} modules: modules.map((mod) => {
return {
throw new Error(`Unable to find the plugin "${pluginName}".`) resolve: `${pluginPath}/${srcDir}/modules/${mod.name}`,
} options: pluginOptions,
}
try { }),
// If the path is absolute, resolve the directory of the internal plugin,
// otherwise resolve the directory containing the package.json
const resolvedPath = require.resolve(pluginName, {
paths: [process.cwd()],
})
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
const computedResolvedPath = path.join(resolvedPath, "dist")
return {
resolve: computedResolvedPath,
id: createPluginId(packageJSON.name),
name: packageJSON.name,
options: {},
version: packageJSON.version,
}
} catch (err) {
throw new Error(
`Unable to find plugin "${pluginName}". Perhaps you need to install its package?`
)
} }
} }
export function getResolvedPlugins( export async function getResolvedPlugins(
rootDirectory: string, rootDirectory: string,
configModule: ConfigModule, configModule: ConfigModule,
isMedusaProject = false isMedusaProject = false
): undefined | PluginDetails[] { ): Promise<PluginDetails[]> {
const resolved = configModule?.plugins?.map((plugin) => { const resolved = await Promise.all(
if (isString(plugin)) { (configModule?.plugins || []).map(async (plugin) => {
return resolvePlugin(plugin) if (isString(plugin)) {
} return resolvePlugin(rootDirectory, plugin)
}
const details = resolvePlugin(plugin.resolve) return resolvePlugin(rootDirectory, plugin.resolve, plugin.options)
details.options = plugin.options })
)
return details
})
if (isMedusaProject) { if (isMedusaProject) {
const extensionDirectoryPath = getExtensionDirectoryPath() const extensionDirectory = path.join(rootDirectory, MEDUSA_APP_SOURCE_PATH)
const extensionDirectory = path.join(rootDirectory, extensionDirectoryPath)
resolved.push({ resolved.push({
resolve: extensionDirectory, resolve: extensionDirectory,
name: MEDUSA_PROJECT_NAME, name: MEDUSA_PROJECT_NAME,
+1 -1
View File
@@ -146,7 +146,7 @@ export default async ({
ContainerRegistrationKeys.CONFIG_MODULE ContainerRegistrationKeys.CONFIG_MODULE
) )
const plugins = getResolvedPlugins(rootDirectory, configModule, true) || [] const plugins = await getResolvedPlugins(rootDirectory, configModule, true)
const linksSourcePaths = plugins.map((plugin) => const linksSourcePaths = plugins.map((plugin) =>
join(plugin.resolve, "links") join(plugin.resolve, "links")
) )