diff --git a/packages/framework/framework/package.json b/packages/framework/framework/package.json index 42b4107491..9417e681f3 100644 --- a/packages/framework/framework/package.json +++ b/packages/framework/framework/package.json @@ -13,6 +13,7 @@ "./logger": "./dist/logger/index.js", "./database": "./dist/database/index.js", "./subscribers": "./dist/subscribers/index.js", + "./links": "./dist/links/index.js", "./jobs": "./dist/jobs/index.js" }, "engines": { @@ -47,6 +48,7 @@ }, "dependencies": { "@medusajs/medusa-cli": "^1.3.22", + "@medusajs/modules-sdk": "^1.12.11", "@medusajs/utils": "^1.11.9", "@medusajs/workflows-sdk": "^0.1.6", "awilix": "^8.0.0", diff --git a/packages/framework/framework/src/container.ts b/packages/framework/framework/src/container.ts index e1a566eb15..22428798bc 100644 --- a/packages/framework/framework/src/container.ts +++ b/packages/framework/framework/src/container.ts @@ -1,3 +1,36 @@ import { createMedusaContainer } from "@medusajs/utils" +import { AwilixContainer, ResolveOptions } from "awilix" + +/** + * The following interface acts as a bucket that other modules or the + * utils package can fill using declaration merging + */ +export interface ModuleImplementations {} + +/** + * The Medusa Container extends [Awilix](https://github.com/jeffijoe/awilix) to + * provide dependency injection functionalities. + */ +export type MedusaContainer = + Omit & { + resolve( + key: K, + resolveOptions?: ResolveOptions + ): Cradle[K] + resolve(key: string, resolveOptions?: ResolveOptions): T + + /** + * @ignore + */ + registerAdd: (name: string, registration: T) => MedusaContainer + /** + * @ignore + */ + createScope: () => MedusaContainer + } + +export type ContainerLike = { + resolve(key: string): T +} export const container = createMedusaContainer() diff --git a/packages/framework/framework/src/database/pg-connection-loader.ts b/packages/framework/framework/src/database/pg-connection-loader.ts index 188cb87b92..07f4757bcd 100644 --- a/packages/framework/framework/src/database/pg-connection-loader.ts +++ b/packages/framework/framework/src/database/pg-connection-loader.ts @@ -10,7 +10,9 @@ export function pgConnectionLoader(): ReturnType< typeof ModulesSdkUtils.createPgConnection > { if (container.hasRegistration(ContainerRegistrationKeys.PG_CONNECTION)) { - return container.resolve(ContainerRegistrationKeys.PG_CONNECTION) + return container.resolve( + ContainerRegistrationKeys.PG_CONNECTION + ) as unknown as ReturnType } const configModule = configManager.config diff --git a/packages/framework/framework/src/index.ts b/packages/framework/framework/src/index.ts index d9abdef176..fc71a46e4a 100644 --- a/packages/framework/framework/src/index.ts +++ b/packages/framework/framework/src/index.ts @@ -4,5 +4,6 @@ export * from "./http" export * from "./database" export * from "./container" export * from "./subscribers" +export * from "./links" export * from "./jobs" export * from "./feature-flags" diff --git a/packages/framework/framework/src/jobs/__fixtures__/mock-scheduler-storage.ts b/packages/framework/framework/src/jobs/__fixtures__/mock-scheduler-storage.ts index 772da63aa3..009102ef12 100644 --- a/packages/framework/framework/src/jobs/__fixtures__/mock-scheduler-storage.ts +++ b/packages/framework/framework/src/jobs/__fixtures__/mock-scheduler-storage.ts @@ -8,14 +8,14 @@ export class MockSchedulerStorage implements IDistributedSchedulerStorage { jobDefinition: string | { jobId: string }, schedulerOptions: SchedulerOptions ): Promise { - return Promise.resolve() + return await Promise.resolve() } async remove(jobId: string): Promise { - return Promise.resolve() + return await Promise.resolve() } async removeAll(): Promise { - return Promise.resolve() + return await Promise.resolve() } } diff --git a/packages/framework/framework/src/links/__fixtures__/links/link.ts b/packages/framework/framework/src/links/__fixtures__/links/link.ts new file mode 100644 index 0000000000..e4e65253db --- /dev/null +++ b/packages/framework/framework/src/links/__fixtures__/links/link.ts @@ -0,0 +1,22 @@ +import { defineLink, MedusaService, model, Module } from "@medusajs/utils" + +const model1 = model.define("model-1", { + id: model.id().primaryKey(), +}) + +const model2 = model.define("model-2", { + id: model.id().primaryKey(), +}) + +const module1 = Module("module-1", { + service: class Service1 extends MedusaService({ model1 }) {}, +}) + +const module2 = Module("module-2", { + service: class Service2 extends MedusaService({ model2 }) {}, +}) + +export const module1And2Link = defineLink( + module1.linkable.model1, + module2.linkable.model2 +) diff --git a/packages/framework/framework/src/links/__fixtures__/links/sub-links/link.ts b/packages/framework/framework/src/links/__fixtures__/links/sub-links/link.ts new file mode 100644 index 0000000000..2da6e96940 --- /dev/null +++ b/packages/framework/framework/src/links/__fixtures__/links/sub-links/link.ts @@ -0,0 +1,22 @@ +import { defineLink, MedusaService, model, Module } from "@medusajs/utils" + +const model3 = model.define("model-3", { + id: model.id().primaryKey(), +}) + +const model4 = model.define("model-4", { + id: model.id().primaryKey(), +}) + +const module3 = Module("module-3", { + service: class Service3 extends MedusaService({ model3 }) {}, +}) + +const module4 = Module("module-4", { + service: class Service4 extends MedusaService({ model4 }) {}, +}) + +export const module3And4Link = defineLink( + module3.linkable.model3, + module4.linkable.model4 +) diff --git a/packages/framework/framework/src/links/__tests__/index.spec.ts b/packages/framework/framework/src/links/__tests__/index.spec.ts new file mode 100644 index 0000000000..ada38d3ce9 --- /dev/null +++ b/packages/framework/framework/src/links/__tests__/index.spec.ts @@ -0,0 +1,19 @@ +import { join } from "path" +import { LinkLoader } from "../link-loader" +import { MedusaModule } from "@medusajs/modules-sdk" + +describe("LinkLoader", () => { + const rootDir = join(__dirname, "../__fixtures__", "links") + + it("should register each link in the '/links' folder and sub folder", async () => { + let links = MedusaModule.getCustomLinks() + + expect(links.length).toBe(0) + + await new LinkLoader(rootDir).load() + + links = MedusaModule.getCustomLinks() + + expect(links.length).toBe(2) + }) +}) diff --git a/packages/framework/framework/src/links/index.ts b/packages/framework/framework/src/links/index.ts new file mode 100644 index 0000000000..039e93e377 --- /dev/null +++ b/packages/framework/framework/src/links/index.ts @@ -0,0 +1 @@ +export * from "./link-loader" diff --git a/packages/framework/framework/src/links/link-loader.ts b/packages/framework/framework/src/links/link-loader.ts new file mode 100644 index 0000000000..9ffa08633e --- /dev/null +++ b/packages/framework/framework/src/links/link-loader.ts @@ -0,0 +1,71 @@ +import { promiseAll } from "@medusajs/utils" +import { logger } from "../logger" +import { access, readdir } from "fs/promises" +import { join } from "path" + +export class LinkLoader { + /** + * The directory from which to load the links + * @private + */ + #sourceDir: string | string[] + + /** + * The list of file names to exclude from the subscriber scan + * @private + */ + #excludes: RegExp[] = [ + /index\.js/, + /index\.ts/, + /\.DS_Store/, + /(\.ts\.map|\.js\.map|\.d\.ts|\.md)/, + /^_[^/\\]*(\.[^/\\]+)?$/, + ] + + constructor(sourceDir: string | string[]) { + this.#sourceDir = sourceDir + } + + /** + * Load links from the source paths, links are registering themselves, + * therefore we only need to import them + */ + async load() { + const normalizedSourcePath = Array.isArray(this.#sourceDir) + ? this.#sourceDir + : [this.#sourceDir] + + const promises = normalizedSourcePath.map(async (sourcePath) => { + try { + await access(sourcePath) + } catch { + return + } + + return await readdir(sourcePath, { + recursive: true, + withFileTypes: true, + }).then(async (entries) => { + const fileEntries = entries.filter((entry) => { + return ( + !entry.isDirectory() && + !this.#excludes.some((exclude) => exclude.test(entry.name)) + ) + }) + + logger.debug(`Registering links from ${sourcePath}.`) + + return await promiseAll( + fileEntries.map(async (entry) => { + const fullPath = join(entry.path, entry.name) + return await import(fullPath) + }) + ) + }) + }) + + await promiseAll(promises) + + logger.debug(`Links registered.`) + } +} diff --git a/packages/medusa/src/commands/links.ts b/packages/medusa/src/commands/links.ts index 700b814b34..47fa930086 100644 --- a/packages/medusa/src/commands/links.ts +++ b/packages/medusa/src/commands/links.ts @@ -2,13 +2,13 @@ import boxen from "boxen" import chalk from "chalk" import checkbox from "@inquirer/checkbox" -import { logger } from "@medusajs/framework" +import { LinkLoader, logger } from "@medusajs/framework" import { initializeContainer } from "../loaders" import { ContainerRegistrationKeys } from "@medusajs/utils" import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins" -import { resolvePluginsLinks } from "../loaders/helpers/resolve-plugins-links" import { getLinksExecutionPlanner } from "../loaders/medusa-app" import { LinkMigrationsPlannerAction } from "@medusajs/types" +import { join } from "path" type Action = "sync" @@ -103,11 +103,13 @@ const main = async function ({ directory }) { ) const plugins = getResolvedPlugins(directory, configModule, true) || [] - const pluginLinks = await resolvePluginsLinks(plugins, container) + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() const planner = await getLinksExecutionPlanner({ configModule, - linkModules: pluginLinks, container, }) diff --git a/packages/medusa/src/commands/migrate.ts b/packages/medusa/src/commands/migrate.ts index 85d4d7e9ac..a29b77c303 100644 --- a/packages/medusa/src/commands/migrate.ts +++ b/packages/medusa/src/commands/migrate.ts @@ -1,9 +1,9 @@ -import { logger } from "@medusajs/framework" +import { LinkLoader, logger } from "@medusajs/framework" import { runMedusaAppMigrations } from "../loaders/medusa-app" import { initializeContainer } from "../loaders" import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins" -import { resolvePluginsLinks } from "../loaders/helpers/resolve-plugins-links" +import { join } from "path" const TERMINAL_SIZE = process.stdout.columns @@ -52,14 +52,16 @@ const main = async function ({ directory }) { ) const plugins = getResolvedPlugins(directory, configModule, true) || [] - const pluginLinks = await resolvePluginsLinks(plugins, container) + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() if (action === "run") { logger.info("Running migrations...") await runMedusaAppMigrations({ configModule, - linkModules: pluginLinks, container, action: "run", }) @@ -74,7 +76,6 @@ const main = async function ({ directory }) { await runMedusaAppMigrations({ moduleNames: modules, configModule, - linkModules: pluginLinks, container, action: "revert", }) @@ -100,7 +101,6 @@ const main = async function ({ directory }) { await runMedusaAppMigrations({ moduleNames: modules, configModule, - linkModules: pluginLinks, container, action: "generate", }) diff --git a/packages/medusa/src/loaders/__tests__/resolve-plugins-links.spec.ts b/packages/medusa/src/loaders/__tests__/resolve-plugins-links.spec.ts deleted file mode 100644 index 3e82fa0942..0000000000 --- a/packages/medusa/src/loaders/__tests__/resolve-plugins-links.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { mkdirSync, rmSync, writeFileSync } from "fs" -import { resolve } from "path" -import { resolvePluginsLinks } from "../helpers/resolve-plugins-links" -import { createMedusaContainer } from "@medusajs/utils" -import { asValue } from "awilix" - -const distTestTargetDirectorPath = resolve(__dirname, "__links__") - -const getFolderTestTargetDirectoryPath = (folderName: string): string => { - return resolve(distTestTargetDirectorPath, folderName) -} - -describe("resolve plugins links", () => { - beforeEach(() => { - jest.resetModules() - jest.clearAllMocks() - - rmSync(distTestTargetDirectorPath, { recursive: true, force: true }) - - mkdirSync(getFolderTestTargetDirectoryPath("links"), { - mode: "777", - recursive: true, - }) - }) - - afterAll(() => { - rmSync(distTestTargetDirectorPath, { recursive: true, force: true }) - }) - - it("should load the custom links from the links directory", async () => { - writeFileSync( - resolve(getFolderTestTargetDirectoryPath("links"), "link.js"), - ` - export default { - isLink: true - } - ` - ) - - writeFileSync( - resolve(getFolderTestTargetDirectoryPath("links"), "empty-link.js"), - ` - export default 'string' - ` - ) - - const loggerMock = { warn: jest.fn() } - const container = createMedusaContainer() - container.register({ - logger: asValue(loggerMock), - }) - - const links = await resolvePluginsLinks( - [ - { - resolve: distTestTargetDirectorPath, - }, - ], - container - ) - - expect(loggerMock.warn).toHaveBeenCalledTimes(1) - expect(loggerMock.warn).toHaveBeenCalledWith( - `Links file ${distTestTargetDirectorPath}/links/empty-link.js does not export a default object` - ) - - expect(links).toEqual([ - { - isLink: true, - }, - ]) - }) -}) diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins-links.ts b/packages/medusa/src/loaders/helpers/resolve-plugins-links.ts deleted file mode 100644 index 4d159bf274..0000000000 --- a/packages/medusa/src/loaders/helpers/resolve-plugins-links.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { glob } from "glob" -import { - MedusaContainer, - ModuleJoinerConfig, - PluginDetails, -} from "@medusajs/types" -import { - ContainerRegistrationKeys, - DefineLinkSymbol, - isObject, -} from "@medusajs/utils" - -/** - * import files from the links directory to retrieve the links to be loaded - * @param plugins - * @param container - */ -export async function resolvePluginsLinks( - plugins: PluginDetails[], - container: MedusaContainer -): Promise { - const logger = - container.resolve(ContainerRegistrationKeys.LOGGER, { - allowUnregistered: true, - }) ?? console - return ( - await Promise.all( - plugins.map(async (pluginDetails) => { - const files = glob.sync( - `${pluginDetails.resolve}/links/*.{ts,js,mjs,mts}`, - { - ignore: ["**/*.d.ts", "**/*.map"], - } - ) - return ( - await Promise.all( - files.map(async (file) => { - const import_ = await import(file) - if (import_.default && !isObject(import_.default)) { - logger.warn( - `Links file ${file} does not export a default object` - ) - return - } - - return import_.default - }) - ) - ).filter((value) => { - return isObject(value) && !value[DefineLinkSymbol] - }) - }) - ) - ) - .flat(Infinity) - .filter(Boolean) -} diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index fa21c20d61..d514c9a094 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -13,6 +13,7 @@ import { container, expressLoader, featureFlagsLoader, + LinkLoader, JobLoader, logger, pgConnectionLoader, @@ -20,7 +21,6 @@ import { } from "@medusajs/framework" import { registerWorkflows } from "./helpers/register-workflows" import { getResolvedPlugins } from "./helpers/resolve-plugins" -import { resolvePluginsLinks } from "./helpers/resolve-plugins-links" import loadMedusaApp from "./medusa-app" type Options = { @@ -151,7 +151,10 @@ export default async ({ ) const plugins = getResolvedPlugins(rootDirectory, configModule, true) || [] - const pluginLinks = await resolvePluginsLinks(plugins, container) + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() const { onApplicationStart, @@ -159,7 +162,6 @@ export default async ({ onApplicationPrepareShutdown, } = await loadMedusaApp({ container, - linkModules: pluginLinks, }) await registerWorkflows(plugins) diff --git a/yarn.lock b/yarn.lock index f12bb6d14e..c2a8b53385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4610,6 +4610,7 @@ __metadata: resolution: "@medusajs/framework@workspace:packages/framework/framework" dependencies: "@medusajs/medusa-cli": ^1.3.22 + "@medusajs/modules-sdk": ^1.12.11 "@medusajs/types": ^1.11.16 "@medusajs/utils": ^1.11.9 "@medusajs/workflows-sdk": ^0.1.6