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

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

View File

@@ -922,10 +922,50 @@ export type ConfigModule = {
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 = {
resolve: string
name: string
id: string
options: Record<string, unknown>
version: string
modules?: InputConfig["modules"]
}

View File

@@ -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<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
* 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] },

View File

@@ -2,21 +2,51 @@ import { Dirent } from "fs"
import { readdir } from "fs/promises"
import { join } from "path"
export async function readDirRecursive(dir: string): Promise<Dirent[]> {
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<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()) {
await readRecursive(fullPath)
if (entry.isDirectory()) {
await readRecursive(fullPath)
}
}
} catch (error) {
if (options?.ignoreMissing && error.code === "ENOENT") {
return
}
throw error
}
}

View File

@@ -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")
)

View File

@@ -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",

View File

@@ -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")
)

View File

@@ -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")
)

View File

@@ -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")
)

View File

@@ -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")
)

View File

@@ -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`
)
})
})

View 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 { 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<string, unknown>
version: string
} {
if (!isAbsolute(pluginName)) {
let resolvedPath = path.resolve(`./plugins/${pluginName}`)
const doesExistsInPlugin = existsSync(resolvedPath)
async function resolvePlugin(
rootDirectory: string,
pluginPath: string,
options?: any
): Promise<PluginDetails> {
const pkgJSON = await resolvePluginPkgFile(rootDirectory, pluginPath)
const resolvedPath = path.dirname(pkgJSON.path)
if (doesExistsInPlugin) {
return loadPluginDetails({
pluginName,
resolvedPath,
})
}
const name = pkgJSON.contents.name || pluginPath
const srcDir = pkgJSON.contents.main
? path.dirname(pkgJSON.contents.main)
: "build"
// Find the plugin in the file system
resolvedPath = path.resolve(pluginName)
const doesExistsInFileSystem = existsSync(resolvedPath)
const resolve = path.join(resolvedPath, srcDir)
const modules = await readDir(path.join(resolve, "modules"), {
ignoreMissing: true,
})
const pluginOptions = options ?? {}
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?`
)
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<PluginDetails[]> {
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,

View File

@@ -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")
)