From 0f264d8f5c7becdbfbd2dd6c548ba96e25d40e6e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 8 Jan 2025 16:01:37 +0530 Subject: [PATCH 1/4] feat: remove dead code and refactor the logic of resolving plugins --- .../core/types/src/common/config-module.ts | 40 ++++ .../core/utils/src/common/define-config.ts | 44 +---- .../utils/src/common/read-dir-recursive.ts | 52 ++++-- packages/medusa/package.json | 2 +- packages/medusa/src/commands/db/generate.ts | 2 +- packages/medusa/src/commands/db/migrate.ts | 2 +- packages/medusa/src/commands/db/rollback.ts | 2 +- packages/medusa/src/commands/db/sync-links.ts | 2 +- .../__tests__/get-resolved-plugins.spec.ts | 167 +++++++++++++++++ .../src/loaders/helpers/resolve-plugins.ts | 173 ++++++------------ packages/medusa/src/loaders/index.ts | 2 +- 11 files changed, 311 insertions(+), 177 deletions(-) create mode 100644 packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts 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 25fe76a99205b..2638dada6b059 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, ...restOfProjectConfig } = config.projectConfig || {} /** @@ -132,14 +96,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/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..68ad686d77e0f --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts @@ -0,0 +1,167 @@ +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", + }, + ], + }, + ]) + }) +}) + +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", + }, + ], + }, + ]) + }) +}) diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins.ts b/packages/medusa/src/loaders/helpers/resolve-plugins.ts index 0d599e5dbb2e4..134df20485161 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -1,148 +1,81 @@ -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" +import path from "path" +import { isString, readDir } from "@medusajs/framework/utils" +import { ConfigModule, PluginDetails } from "@medusajs/framework/types" export const MEDUSA_PROJECT_NAME = "project-plugin" +const MEDUSA_APP_SOURCE_PATH = "src" + 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 - */ -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`) - ) - 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, `**`), - } - } - - // 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 { + /** + * The package.json file should be requirable in order to resolve + * the plugin + */ + const pkgJSONPath = require.resolve(path.join(pluginPath, "package.json"), { + paths: [rootDirectory], + }) + const resolvedPath = path.dirname(pkgJSONPath) + const packageJSON = JSON.parse(fs.readFileSync(pkgJSONPath, "utf-8")) + + const name = packageJSON.name || pluginPath + const srcDir = packageJSON.main ? path.dirname(packageJSON.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: packageJSON.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") ) From f3620b87256a20bd30ac1a620eb03931775006d4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 8 Jan 2025 17:01:42 +0530 Subject: [PATCH 2/4] fix: test-utils to await getResolvedPlugins --- .../medusa-test-utils/src/medusa-test-runner-utils/use-db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") ) From b70276eec83527133f2c568565d99571e9c1b8c9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 8 Jan 2025 18:18:09 +0530 Subject: [PATCH 3/4] Create tiny-moles-build.md --- .changeset/tiny-moles-build.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/tiny-moles-build.md 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 From 267a73f423e6f35a76e9d74572aa448b21d69644 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 8 Jan 2025 18:48:41 +0530 Subject: [PATCH 4/4] refactor: reword missing file errors --- .../__tests__/get-resolved-plugins.spec.ts | 44 ++++++++++++++++ .../src/loaders/helpers/resolve-plugins.ts | 50 +++++++++++++------ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts index 68ad686d77e0f..2aa6cbd8f2423 100644 --- a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts +++ b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts @@ -83,6 +83,28 @@ describe("getResolvedPlugins | relative paths", () => { }, ]) }) + + 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", () => { @@ -164,4 +186,26 @@ describe("getResolvedPlugins | package reference", () => { }, ]) }) + + 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 134df20485161..529c9b9ac5655 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -1,10 +1,10 @@ -import fs from "fs" import path from "path" +import fs from "fs/promises" import { isString, readDir } from "@medusajs/framework/utils" import { ConfigModule, PluginDetails } from "@medusajs/framework/types" -export const MEDUSA_PROJECT_NAME = "project-plugin" const MEDUSA_APP_SOURCE_PATH = "src" +export const MEDUSA_PROJECT_NAME = "project-plugin" function createPluginId(name: string): string { return name @@ -14,6 +14,32 @@ function createFileContentHash(path: string, files: string): string { return path + files } +/** + * Returns the absolute path to the package.json file for a + * given plugin identifier. + */ +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") + ) + 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 + } +} + /** * 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 @@ -27,18 +53,14 @@ async function resolvePlugin( pluginPath: string, options?: any ): Promise { - /** - * The package.json file should be requirable in order to resolve - * the plugin - */ - const pkgJSONPath = require.resolve(path.join(pluginPath, "package.json"), { - paths: [rootDirectory], - }) - const resolvedPath = path.dirname(pkgJSONPath) - const packageJSON = JSON.parse(fs.readFileSync(pkgJSONPath, "utf-8")) + 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 name = packageJSON.name || pluginPath - const srcDir = packageJSON.main ? path.dirname(packageJSON.main) : "build" const resolve = path.join(resolvedPath, srcDir) const modules = await readDir(path.join(resolve, "modules"), { ignoreMissing: true, @@ -50,7 +72,7 @@ async function resolvePlugin( name, id: createPluginId(name), options: pluginOptions, - version: packageJSON.version || "0.0.0", + version: pkgJSON.contents.version || "0.0.0", modules: modules.map((mod) => { return { resolve: `${pluginPath}/${srcDir}/modules/${mod.name}`,