Files
medusa-store/packages/medusa/src/loaders/plugins.js
Sebastian Rindom 0b8f1a5c31 adds wishlist plugin
2020-07-20 13:04:29 +02:00

351 lines
11 KiB
JavaScript

import glob from "glob"
import {
BaseModel,
BaseService,
PaymentService,
FulfillmentService,
FileService,
} from "medusa-interfaces"
import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
import _ from "lodash"
import path from "path"
import fs from "fs"
import { asFunction, aliasTo } from "awilix"
import { sync as existsSync } from "fs-exists-cached"
/**
* Registers all services in the services directory
*/
export default ({ rootDirectory, container, app }) => {
const { configModule, configFilePath } = getConfigFile(
rootDirectory,
`medusa-config`
)
if (!configModule) {
return
}
const { plugins } = configModule
const resolved = plugins.map(plugin => {
if (_.isString(plugin)) {
return resolvePlugin(plugin)
}
const details = resolvePlugin(plugin.resolve)
details.options = plugin.options
return details
})
resolved.push({
resolve: `${rootDirectory}/dist`,
name: `project-plugin`,
id: createPluginId(`project-plugin`),
options: {},
version: createFileContentHash(process.cwd(), `**`),
})
resolved.forEach(pluginDetails => {
registerModels(pluginDetails, container)
registerServices(pluginDetails, container)
registerMedusaApi(pluginDetails, container)
registerApi(pluginDetails, app)
registerCoreRouters(pluginDetails, container)
registerSubscribers(pluginDetails, container)
})
}
function registerMedusaApi(pluginDetails, container) {
registerMedusaMiddleware(pluginDetails, container)
}
function registerMedusaMiddleware(pluginDetails, container) {
let module
try {
module = require(`${pluginDetails.resolve}/api/medusa-middleware`).default
} catch (err) {
return
}
const middlewareService = container.resolve("middlewareService")
if (module.postAuthentication) {
middlewareService.addPostAuthentication(
module.postAuthentication,
pluginDetails.options
)
}
if (module.preAuthentication) {
middlewareService.addPreAuthentication(
module.preAuthentication,
pluginDetails.options
)
}
}
function registerCoreRouters(pluginDetails, container) {
const middlewareService = container.resolve("middlewareService")
const { resolve } = pluginDetails
const adminFiles = glob.sync(`${resolve}/api/admin/[!__]*.js`, {})
const storeFiles = glob.sync(`${resolve}/api/store/[!__]*.js`, {})
adminFiles.forEach(fn => {
const descriptor = fn.split(".")[0]
const splat = descriptor.split("/")
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
const loaded = require(fn).default
middlewareService.addRouter(path, loaded())
})
storeFiles.forEach(fn => {
const descriptor = fn.split(".")[0]
const splat = descriptor.split("/")
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
const loaded = require(fn).default
middlewareService.addRouter(path, loaded())
})
}
/**
* Registers the plugin's api routes.
*/
function registerApi(pluginDetails, app) {
try {
const routes = require(`${pluginDetails.resolve}/api`).default
app.use("/", routes())
return app
} catch (err) {
return app
}
}
/**
* Registers a service at the right location in our container. If the service is
* a BaseService instance it will be available directly from the container.
* PaymentService instances are added to the paymentProviders array in the
* container. Names are camelCase formatted and namespaced by the folder i.e:
* services/example-payments -> examplePaymentsService
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerServices(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
files.forEach(fn => {
const loaded = require(fn).default
const name = formatRegistrationName(fn)
if (!(loaded.prototype instanceof BaseService)) {
const logger = container.resolve("logger")
const message = `Services must inherit from BaseService, please check ${fn}`
logger.error(message)
throw new Error(message)
}
if (loaded.prototype instanceof PaymentService) {
// Register our payment providers to paymentProviders
container.registerAdd(
"paymentProviders",
asFunction(cradle => new loaded(cradle, pluginDetails.options))
)
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
[`pp_${loaded.identifier}`]: aliasTo(name),
})
} else if (loaded.prototype instanceof FulfillmentService) {
// Register our payment providers to paymentProviders
container.registerAdd(
"fulfillmentProviders",
asFunction(cradle => new loaded(cradle, pluginDetails.options))
)
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
[`fp_${loaded.identifier}`]: aliasTo(name),
})
} else if (loaded.prototype instanceof FileService) {
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
[`fileService`]: aliasTo(name),
})
} else {
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
})
}
})
}
/**
* Registers a plugin's models at the right location in our container. Models
* must inherit from BaseModel. Models are registered directly in the container.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerSubscribers(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/subscribers/*.js`, {})
files.forEach(fn => {
const loaded = require(fn).default
const name = formatRegistrationName(fn)
container.build(
asFunction(
cradle => new loaded(cradle, pluginDetails.options)
).singleton()
)
})
}
/**
* Registers a plugin's models at the right location in our container. Models
* must inherit from BaseModel. Models are registered directly in the container.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerModels(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {})
files.forEach(fn => {
const loaded = require(fn).default
if (!(loaded.prototype instanceof BaseModel)) {
const logger = container.resolve("logger")
const message = `Models must inherit from BaseModel, please check ${fn}`
logger.error(message)
throw new Error(message)
}
const name = formatRegistrationName(fn)
container.register({
[name]: asFunction(
cradle => new loaded(cradle, pluginDetails.options)
).singleton(),
})
})
}
/**
* Formats a filename into the correct container resolution name.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {string} fn - the full path of the file
* @return {string} the formatted name
*/
function formatRegistrationName(fn) {
const descriptor = fn.split(".")[0]
const splat = descriptor.split("/")
const rawname = splat[splat.length - 1]
const namespace = splat[splat.length - 2]
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.slice(1, -1)
const parts = rawname.split("-").map((n, index) => {
if (index !== 0) {
return n.charAt(0).toUpperCase() + n.slice(1)
}
return n
})
return parts.join("") + upperNamespace
}
// TODO: Create unique id for each plugin
function createPluginId(name) {
return name
}
/**
* 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
* the name of the folder where the plugin is contained.
* @return {object} the plugin details
*/
function resolvePlugin(pluginName) {
// Only find plugins when we're not given an absolute path
if (!existsSync(pluginName)) {
// Find the plugin in the local plugins folder
const resolvedPath = path.resolve(`./plugins/${pluginName}`)
if (existsSync(resolvedPath)) {
if (existsSync(`${resolvedPath}/package.json`)) {
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
const name = packageJSON.name || pluginName
//warnOnIncompatiblePeerDependency(name, packageJSON)
return {
resolve: resolvedPath,
name,
id: createPluginId(name),
options: {},
version:
packageJSON.version || createFileContentHash(resolvedPath, `**`),
}
} else {
// Make package.json a requirement for local plugins too
throw new Error(`Plugin ${pluginName} requires a package.json file`)
}
}
}
const rootDir = path.resolve(".")
/**
* Here we have an absolute path to an internal plugin, or a name of a module
* which should be located in node_modules.
*/
try {
const requireSource =
rootDir !== null
? createRequireFromPath(`${rootDir}/:internal:`)
: require
// If the path is absolute, resolve the directory of the internal plugin,
// otherwise resolve the directory containing the package.json
const resolvedPath = path.dirname(
requireSource.resolve(`${pluginName}/package.json`)
)
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
// warnOnIncompatiblePeerDependency(packageJSON.name, packageJSON)
return {
resolve: resolvedPath,
id: createPluginId(packageJSON.name),
name: packageJSON.name,
version: packageJSON.version,
}
} catch (err) {
throw new Error(
`Unable to find plugin "${pluginName}". Perhaps you need to install its package?`
)
}
}
function createFileContentHash(path, files) {
return path + files
}