diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index b93ec6f997..4b8bca2e55 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -1,3 +1,4 @@ +export * from "./auth" export * from "./api-key" export * from "./customer" export * from "./customer-group" diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index bca7a787fe..3741264d37 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -35,6 +35,7 @@ import searchIndexLoader from "./search-index" import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" +import medusaProjectApisLoader from "./load-medusa-project-apis" type Options = { directory: string @@ -81,7 +82,12 @@ async function loadLegacyModulesEntities(configModules, container) { } } -async function loadMedusaV2({ configModule, featureFlagRouter, expressApp }) { +async function loadMedusaV2({ + rootDirectory, + configModule, + featureFlagRouter, + expressApp, +}) { const container = createMedusaContainer() // Add additional information to context of request @@ -124,6 +130,14 @@ async function loadMedusaV2({ configModule, featureFlagRouter, expressApp }) { featureFlagRouter, }) + medusaProjectApisLoader({ + rootDirectory, + container, + app: expressApp, + configModule, + activityId: "medusa-project-apis", + }) + return { container, app: expressApp, @@ -146,7 +160,12 @@ export default async ({ track("FEATURE_FLAGS_LOADED") if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - return await loadMedusaV2({ configModule, featureFlagRouter, expressApp }) + return await loadMedusaV2({ + rootDirectory, + configModule, + featureFlagRouter, + expressApp, + }) } const container = createMedusaContainer() diff --git a/packages/medusa/src/loaders/load-medusa-project-apis.ts b/packages/medusa/src/loaders/load-medusa-project-apis.ts new file mode 100644 index 0000000000..3279758d63 --- /dev/null +++ b/packages/medusa/src/loaders/load-medusa-project-apis.ts @@ -0,0 +1,200 @@ +import { promiseAll } from "@medusajs/utils" +import { Express } from "express" +import glob from "glob" +import _ from "lodash" +import { trackInstallation } from "medusa-telemetry" +import { EOL } from "os" +import path from "path" +import { ConfigModule, Logger, MedusaContainer } from "../types/global" +import ScheduledJobsLoader from "./helpers/jobs" +import { RoutesLoader } from "./helpers/routing" +import { SubscriberLoader } from "./helpers/subscribers" +import logger from "./logger" + +type Options = { + rootDirectory: string + container: MedusaContainer + configModule: ConfigModule + app: Express + activityId: string +} + +type PluginDetails = { + resolve: string + name: string + id: string + options: Record + version: string +} + +export const MEDUSA_PROJECT_NAME = "project-plugin" + +/** + * Registers all services in the services directory + */ +export default async ({ + rootDirectory, + container, + app, + configModule, + activityId, +}: Options): Promise => { + const resolved = getResolvedPlugins(rootDirectory, configModule) || [] + + await promiseAll( + resolved.map(async (pluginDetails) => { + await registerApi(pluginDetails, app, container, configModule, activityId) + await registerSubscribers(pluginDetails, container, activityId) + await registerWorkflows(pluginDetails) + }) + ) + + await promiseAll( + resolved.map(async (pluginDetails) => runLoaders(pluginDetails, container)) + ) + + if (configModule.projectConfig.redis_url) { + await Promise.all( + resolved.map(async (pluginDetails) => { + await registerScheduledJobs(pluginDetails, container) + }) + ) + } else { + logger.warn( + "You don't have Redis configured. Scheduled jobs will not be enabled." + ) + } + + resolved.forEach((plugin) => trackInstallation(plugin.name, "plugin")) +} + +function getResolvedPlugins( + rootDirectory: string, + configModule: ConfigModule, + extensionDirectoryPath = "dist" +): undefined | PluginDetails[] { + const extensionDirectory = path.join(rootDirectory, extensionDirectoryPath) + return [ + { + resolve: extensionDirectory, + name: MEDUSA_PROJECT_NAME, + id: createPluginId(MEDUSA_PROJECT_NAME), + options: configModule, + version: createFileContentHash(process.cwd(), `**`), + }, + ] +} + +async function runLoaders( + pluginDetails: PluginDetails, + container: MedusaContainer +): Promise { + const loaderFiles = glob.sync( + `${pluginDetails.resolve}/loaders/[!__]*.js`, + {} + ) + await promiseAll( + loaderFiles.map(async (loader) => { + try { + const module = require(loader).default + if (typeof module === "function") { + await module(container, pluginDetails.options) + } + } catch (err) { + const logger = container.resolve("logger") + logger.warn(`Running loader failed: ${err.message}`) + return Promise.resolve() + } + }) + ) +} + +async function registerScheduledJobs( + pluginDetails: PluginDetails, + container: MedusaContainer +): Promise { + await new ScheduledJobsLoader( + path.join(pluginDetails.resolve, "jobs"), + container, + pluginDetails.options + ).load() +} + +/** + * Registers the plugin's api routes. + */ +async function registerApi( + pluginDetails: PluginDetails, + app: Express, + container: MedusaContainer, + configmodule: ConfigModule, + activityId: string +): Promise { + const logger = container.resolve("logger") + const projectName = + pluginDetails.name === MEDUSA_PROJECT_NAME + ? "your Medusa project" + : `${pluginDetails.name}` + + logger.progress(activityId, `Registering custom endpoints for ${projectName}`) + + try { + /** + * Register the plugin's API routes using the file based routing. + */ + await new RoutesLoader({ + app, + rootDir: path.join(pluginDetails.resolve, "api"), + activityId: activityId, + configModule: configmodule, + }).load() + } catch (err) { + logger.warn( + `An error occurred while registering API Routes in ${projectName}${ + err.stack ? EOL + err.stack : "" + }` + ) + } + + return app +} + +/** + * Registers a plugin's subscribers at the right location in our container. + * Subscribers are registered directly in the container. + * @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} + */ +async function registerSubscribers( + pluginDetails: PluginDetails, + container: MedusaContainer, + activityId: string +): Promise { + await new SubscriberLoader( + path.join(pluginDetails.resolve, "subscribers"), + container, + pluginDetails.options, + activityId + ).load() +} + +/** + * import files from the workflows directory to run the registration of the wofklows + * @param pluginDetails + */ +async function registerWorkflows(pluginDetails: PluginDetails): Promise { + const files = glob.sync(`${pluginDetails.resolve}/workflows/*.js`, {}) + await Promise.all(files.map(async (file) => import(file))) +} + +// TODO: Create unique id for each plugin +function createPluginId(name: string): string { + return name +} + +function createFileContentHash(path, files): string { + return path + files +} diff --git a/packages/modules-sdk/src/loaders/register-modules.ts b/packages/modules-sdk/src/loaders/register-modules.ts index becced07f7..dee02d61bb 100644 --- a/packages/modules-sdk/src/loaders/register-modules.ts +++ b/packages/modules-sdk/src/loaders/register-modules.ts @@ -1,6 +1,7 @@ import { ExternalModuleDeclaration, InternalModuleDeclaration, + MODULE_RESOURCE_TYPE, MODULE_SCOPE, ModuleDefinition, ModuleExports, @@ -25,7 +26,11 @@ export const registerMedusaModule = ( const modDefinition = definition ?? ModulesDefinition[moduleKey] if (modDefinition === undefined) { - throw new Error(`Module: ${moduleKey} is not defined.`) + moduleResolutions[moduleKey] = getCustomModuleResolution( + moduleKey, + moduleDeclaration as InternalModuleDeclaration + ) + return moduleResolutions } const modDeclaration = @@ -53,6 +58,38 @@ export const registerMedusaModule = ( return moduleResolutions } +function getCustomModuleResolution( + key: string, + moduleConfig: InternalModuleDeclaration | string +): ModuleResolution { + const isString = typeof moduleConfig === "string" + const resolutionPath = resolveCwd( + isString ? moduleConfig : (moduleConfig.resolve as string) + ) + + return { + resolutionPath, + definition: { + key, + label: `Custom: ${key}`, + isRequired: false, + defaultPackage: "", + dependencies: [], + registrationName: key, + defaultModuleDeclaration: { + resources: MODULE_RESOURCE_TYPE.SHARED, + scope: MODULE_SCOPE.INTERNAL, + }, + }, + moduleDeclaration: { + resources: MODULE_RESOURCE_TYPE.SHARED, + scope: MODULE_SCOPE.INTERNAL, + }, + dependencies: [], + options: {}, + } +} + export const registerMedusaLinkModule = ( definition: ModuleDefinition, moduleDeclaration: Partial,