diff --git a/.changeset/tiny-moles-build.md b/.changeset/tiny-moles-build.md new file mode 100644 index 0000000000000..9d63ee6393dea --- /dev/null +++ b/.changeset/tiny-moles-build.md @@ -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 diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index c574f2f57e50f..4dac6def09b6e 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -922,10 +922,50 @@ export type ConfigModule = { featureFlags: Record> } +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 & { + admin: Partial + modules: + | Partial< + InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride + >[] + /** + * @deprecated use the array instead + */ + | ConfigModule["modules"] + } +> + export type PluginDetails = { resolve: string name: string id: string options: Record version: string + modules?: InputConfig["modules"] } diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index fda1b8ec51718..b0bbef36d384c 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -1,6 +1,6 @@ import { ConfigModule, - ExternalModuleDeclaration, + InputConfig, InternalModuleDeclaration, } from "@medusajs/types" import { @@ -29,42 +29,6 @@ export const DEFAULT_STORE_RESTRICTED_FIELDS = [ "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 & { - admin: Partial - modules: - | Partial< - InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride - >[] - /** - * @deprecated use the array instead - */ - | ConfigModule["modules"] - } -> - /** * The "defineConfig" helper can be used to define the configuration * of a medusa application. @@ -73,7 +37,7 @@ type Config = Partial< * make an application work seamlessly, but still provide you the ability * to override configuration as needed. */ -export function defineConfig(config: Config = {}): ConfigModule { +export function defineConfig(config: InputConfig = {}): ConfigModule { const { http, redisOptions, ...restOfProjectConfig } = config.projectConfig || {} @@ -150,14 +114,14 @@ export function defineConfig(config: Config = {}): ConfigModule { * @param configModules */ function resolveModules( - configModules: Config["modules"] + configModules: InputConfig["modules"] ): ConfigModule["modules"] { /** * The default set of modules to always use. The end user can swap * the modules by providing an alternate implementation via their * 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.EVENT_BUS] }, { resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] }, diff --git a/packages/core/utils/src/common/read-dir-recursive.ts b/packages/core/utils/src/common/read-dir-recursive.ts index 7ad9d2a7b43f2..bc5c175c378c0 100644 --- a/packages/core/utils/src/common/read-dir-recursive.ts +++ b/packages/core/utils/src/common/read-dir-recursive.ts @@ -2,21 +2,51 @@ import { Dirent } from "fs" import { readdir } from "fs/promises" import { join } from "path" -export async function readDirRecursive(dir: string): Promise { - let allEntries: Dirent[] = [] - const readRecursive = async (dir) => { +const MISSING_NODE_ERRORS = ["ENOTDIR", "ENOENT"] + +export async function readDir( + dir: string, + options?: { + ignoreMissing?: boolean + } +) { + try { 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) { - const fullPath = join(dir, entry.name) - Object.defineProperty(entry, "path", { - value: dir, - }) - allEntries.push(entry) +export async function readDirRecursive( + dir: string, + options?: { + ignoreMissing?: boolean + } +): Promise { + 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()) { - await readRecursive(fullPath) + if (entry.isDirectory()) { + await readRecursive(fullPath) + } + } + } catch (error) { + if (options?.ignoreMissing && error.code === "ENOENT") { + return } + throw error } } diff --git a/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts b/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts index eafc848cbe9d0..24f2f5c0acd58 100644 --- a/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts +++ b/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts @@ -61,7 +61,7 @@ async function loadCustomLinks(directory: string, container: MedusaContainer) { const configModule = container.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 71f488ffb5a35..1a75509a76ce2 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -40,7 +40,7 @@ "watch": "tsc --build --watch", "build": "rimraf dist && tsc --build", "serve": "node dist/app.js", - "test": "jest --silent --bail --maxWorkers=50% --forceExit" + "test": "jest --silent=false --bail --maxWorkers=50% --forceExit" }, "devDependencies": { "@medusajs/framework": "^2.2.0", diff --git a/packages/medusa/src/commands/db/generate.ts b/packages/medusa/src/commands/db/generate.ts index aeddb2a07e3de..176fa7896ccb5 100644 --- a/packages/medusa/src/commands/db/generate.ts +++ b/packages/medusa/src/commands/db/generate.ts @@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) { ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/migrate.ts b/packages/medusa/src/commands/db/migrate.ts index b8b086280fff8..f7e7a4a356b97 100644 --- a/packages/medusa/src/commands/db/migrate.ts +++ b/packages/medusa/src/commands/db/migrate.ts @@ -37,7 +37,7 @@ export async function migrate({ ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/rollback.ts b/packages/medusa/src/commands/db/rollback.ts index 61ad13118e9d7..2c09252ed2586 100644 --- a/packages/medusa/src/commands/db/rollback.ts +++ b/packages/medusa/src/commands/db/rollback.ts @@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) { ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/sync-links.ts b/packages/medusa/src/commands/db/sync-links.ts index a151c0f0a95dd..59d6da7008460 100644 --- a/packages/medusa/src/commands/db/sync-links.ts +++ b/packages/medusa/src/commands/db/sync-links.ts @@ -187,7 +187,7 @@ const main = async function ({ directory, executeSafe, executeAll }) { const medusaAppLoader = new MedusaAppLoader() - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts new file mode 100644 index 0000000000000..2aa6cbd8f2423 --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts @@ -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` + ) + }) +}) diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins.ts b/packages/medusa/src/loaders/helpers/resolve-plugins.ts index 0d599e5dbb2e4..529c9b9ac5655 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -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 { 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" + function createPluginId(name: string): string { return name } -function createFileContentHash(path, files): string { +function createFileContentHash(path: string, files: string): string { return path + files } -function getExtensionDirectoryPath() { - return "src" -} - /** - * Load plugin details from a path. Return undefined if does not contains a package.json - * @param pluginName - * @param path - * @param includeExtensionDirectoryPath should include src | dist for the resolved details + * Returns the absolute path to the package.json file for a + * given plugin identifier. */ -function loadPluginDetails({ - pluginName, - resolvedPath, - includeExtensionDirectoryPath, -}: { - pluginName: string - resolvedPath: string - includeExtensionDirectoryPath?: boolean -}) { - if (existsSync(`${resolvedPath}/package.json`)) { - const packageJSON = JSON.parse( - fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) +async function resolvePluginPkgFile( + rootDirectory: string, + pluginPath: string +): Promise<{ path: string; contents: any }> { + try { + const pkgJSONPath = require.resolve(path.join(pluginPath, "package.json"), { + paths: [rootDirectory], + }) + const packageJSONContents = JSON.parse( + await fs.readFile(pkgJSONPath, "utf-8") ) - const name = packageJSON.name || pluginName - - const extensionDirectoryPath = getExtensionDirectoryPath() - const resolve = includeExtensionDirectoryPath - ? path.join(resolvedPath, extensionDirectoryPath) - : resolvedPath - - return { - resolve, - name, - id: createPluginId(name), - options: {}, - version: packageJSON.version || createFileContentHash(path, `**`), + return { path: pkgJSONPath, contents: packageJSONContents } + } catch (error) { + if (error.code === "MODULE_NOT_FOUND" || error.code === "ENOENT") { + throw new Error( + `Unable to resolve plugin "${pluginPath}". Make sure the plugin directory has a package.json file` + ) } + 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 * found in the plugins folder. Otherwise we will look for the plugin in the * 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. * @return {object} the plugin details */ -function resolvePlugin(pluginName: string): { - resolve: string - id: string - name: string - options: Record - version: string -} { - if (!isAbsolute(pluginName)) { - let resolvedPath = path.resolve(`./plugins/${pluginName}`) - const doesExistsInPlugin = existsSync(resolvedPath) - - if (doesExistsInPlugin) { - return loadPluginDetails({ - pluginName, - resolvedPath, - }) - } - - // Find the plugin in the file system - resolvedPath = path.resolve(pluginName) - const doesExistsInFileSystem = existsSync(resolvedPath) - - if (doesExistsInFileSystem) { - return loadPluginDetails({ - pluginName, - resolvedPath, - includeExtensionDirectoryPath: true, - }) - } - - throw new Error(`Unable to find the plugin "${pluginName}".`) - } - - 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?` - ) +async function resolvePlugin( + rootDirectory: string, + pluginPath: string, + options?: any +): Promise { + const pkgJSON = await resolvePluginPkgFile(rootDirectory, pluginPath) + const resolvedPath = path.dirname(pkgJSON.path) + + const name = pkgJSON.contents.name || pluginPath + const srcDir = pkgJSON.contents.main + ? path.dirname(pkgJSON.contents.main) + : "build" + + const resolve = path.join(resolvedPath, srcDir) + const modules = await readDir(path.join(resolve, "modules"), { + ignoreMissing: true, + }) + const pluginOptions = options ?? {} + + return { + resolve, + name, + id: createPluginId(name), + options: pluginOptions, + version: pkgJSON.contents.version || "0.0.0", + modules: modules.map((mod) => { + return { + resolve: `${pluginPath}/${srcDir}/modules/${mod.name}`, + options: pluginOptions, + } + }), } } -export function getResolvedPlugins( +export async function getResolvedPlugins( rootDirectory: string, configModule: ConfigModule, isMedusaProject = false -): undefined | PluginDetails[] { - const resolved = configModule?.plugins?.map((plugin) => { - if (isString(plugin)) { - return resolvePlugin(plugin) - } - - const details = resolvePlugin(plugin.resolve) - details.options = plugin.options - - return details - }) +): Promise { + const resolved = await Promise.all( + (configModule?.plugins || []).map(async (plugin) => { + if (isString(plugin)) { + return resolvePlugin(rootDirectory, plugin) + } + return resolvePlugin(rootDirectory, plugin.resolve, plugin.options) + }) + ) if (isMedusaProject) { - const extensionDirectoryPath = getExtensionDirectoryPath() - const extensionDirectory = path.join(rootDirectory, extensionDirectoryPath) + const extensionDirectory = path.join(rootDirectory, MEDUSA_APP_SOURCE_PATH) resolved.push({ resolve: extensionDirectory, name: MEDUSA_PROJECT_NAME, diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index bd33ce8b1bd0d..e8ea0b2afa594 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -146,7 +146,7 @@ export default async ({ ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(rootDirectory, configModule, true) || [] + const plugins = await getResolvedPlugins(rootDirectory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") )