chore(): start moving some packages to the core directory (#7215)

This commit is contained in:
Adrien de Peretti
2024-05-03 13:37:41 +02:00
committed by GitHub
parent fdee748eed
commit bbccd6481d
1436 changed files with 275 additions and 756 deletions
@@ -0,0 +1,13 @@
const loader = ({}) => {
throw new Error("loader")
}
const service = class TestService {}
const migrations = []
const loaders = [loader]
export default {
service,
migrations,
loaders,
}
@@ -0,0 +1,11 @@
const service = class TestService {}
const migrations = []
const loaders = []
const models = []
export default {
service,
migrations,
loaders,
models,
}
@@ -0,0 +1,7 @@
const migrations = []
const loaders = []
export default {
migrations,
loaders,
}
@@ -0,0 +1,5 @@
const service = class TestService {}
export default {
services: [service],
}
@@ -0,0 +1,5 @@
const service = class TestService {}
export const defaultExport = {
services: [service],
}
@@ -0,0 +1,3 @@
export default {
loaders: [],
}
@@ -0,0 +1,2 @@
export const trackInstallation = jest.fn()
export const trackFeatureFlag = jest.fn()
@@ -0,0 +1,324 @@
import {
InternalModuleDeclaration,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
} from "@medusajs/types"
import { MedusaModule } from "../../medusa-module"
import { asValue } from "awilix"
const mockRegisterMedusaModule = jest.fn().mockImplementation(() => {
return {
moduleKey: {
definition: {
key: "moduleKey",
registrationName: "moduleKey",
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
})
const mockModuleLoader = jest.fn().mockImplementation(({ container }) => {
container.register({
moduleKey: asValue({}),
})
return Promise.resolve({})
})
jest.mock("./../../loaders", () => ({
registerMedusaModule: jest
.fn()
.mockImplementation((...args) => mockRegisterMedusaModule()),
moduleLoader: jest
.fn()
.mockImplementation((...args) => mockModuleLoader.apply(this, args)),
}))
describe("Medusa Modules", () => {
beforeEach(() => {
MedusaModule.clearInstances()
jest.resetModules()
jest.clearAllMocks()
})
it("should create singleton instances", async () => {
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
expect(mockRegisterMedusaModule).toBeCalledTimes(1)
expect(mockModuleLoader).toBeCalledTimes(1)
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
expect(mockRegisterMedusaModule).toBeCalledTimes(2)
expect(mockModuleLoader).toBeCalledTimes(2)
})
it("should prevent the module being loaded multiple times under concurrent requests", async () => {
const load: any = []
for (let i = 5; i--; ) {
load.push(
MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
)
}
const intances = Promise.all(load)
expect(mockRegisterMedusaModule).toBeCalledTimes(1)
expect(mockModuleLoader).toBeCalledTimes(1)
expect(intances[(await intances).length - 1]).toBe(intances[0])
})
it("getModuleInstance should return the first instance of the module if there is none flagged as 'main'", async () => {
const moduleA = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
const moduleB = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleA)
})
it("should return the module flagged as 'main' when multiple instances are available", async () => {
const moduleA = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
const moduleB = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
main: true,
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB)
})
it("should retrieve the module by their given alias", async () => {
const moduleA = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
alias: "mod_A",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
const moduleB = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
main: true,
alias: "mod_B",
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
const moduleC = await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
alias: "mod_C",
options: {
moduleC: true,
},
} as InternalModuleDeclaration,
})
// main
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB)
expect(MedusaModule.getModuleInstance("moduleKey", "mod_A")).toEqual(
moduleA
)
expect(MedusaModule.getModuleInstance("moduleKey", "mod_B")).toEqual(
moduleB
)
expect(MedusaModule.getModuleInstance("moduleKey", "mod_C")).toEqual(
moduleC
)
})
it("should prevent two main modules being set as 'main'", async () => {
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
alias: "mod_A",
options: {
abc: 123,
},
} as InternalModuleDeclaration,
})
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
main: true,
alias: "mod_B",
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
const moduleC = MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
main: true,
alias: "mod_C",
options: {
moduleC: true,
},
} as InternalModuleDeclaration,
})
expect(moduleC).rejects.toThrow(
"Module moduleKey already have a 'main' registered."
)
})
it("should prevent the same alias be used for different instances of the same module", async () => {
await MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
alias: "module_alias",
options: {
different_options: "abc",
},
} as InternalModuleDeclaration,
})
const moduleC = MedusaModule.bootstrap({
moduleKey: "moduleKey",
defaultPath: "@path",
declaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: "@path",
alias: "module_alias",
options: {
moduleC: true,
},
} as InternalModuleDeclaration,
})
expect(moduleC).rejects.toThrow(
"Module moduleKey already registed as 'module_alias'. Please choose a different alias."
)
})
})
@@ -0,0 +1,279 @@
import {
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
ModuleResolution,
} from "@medusajs/types"
import { createMedusaContainer } from "@medusajs/utils"
import { EOL } from "os"
import { moduleLoader } from "../module-loader"
const logger = {
warn: jest.fn(),
error: jest.fn(),
} as any
describe("modules loader", () => {
let container
afterEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
container = createMedusaContainer()
})
it("should register the service as undefined in the container when no resolution path is given", async () => {
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: false,
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
await moduleLoader({ container, moduleResolutions, logger })
const testService = container.resolve(
moduleResolutions.testService.definition.key
)
expect(testService).toBe(undefined)
})
it("should register the service ", async () => {
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/default",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
await moduleLoader({ container, moduleResolutions, logger })
const testService = container.resolve(
moduleResolutions.testService.definition.key,
{}
)
/*
expect(trackInstallation).toHaveBeenCalledWith(
{
module: moduleResolutions.testService.definition.key,
resolution: moduleResolutions.testService.resolutionPath,
},
"module"
)
*/
expect(testService).toBeTruthy()
expect(typeof testService).toEqual("object")
})
it("should run the defined loaders and logs the errors if something fails", async () => {
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/brokenloader",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
await moduleLoader({ container, moduleResolutions, logger })
expect(logger.warn).toHaveBeenCalledWith(
`Could not resolve module: TestService. Error: Loaders for module TestService failed: loader${EOL}`
)
})
it("should log the errors if no service is defined", async () => {
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/no-service",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
await moduleLoader({ container, moduleResolutions, logger })
expect(logger.warn).toHaveBeenCalledWith(
`Could not resolve module: TestService. Error: No service found in module. Make sure your module exports a service.${EOL}`
)
})
it("should throw an error if no service is defined and the module is required", async () => {
expect.assertions(1)
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/no-service",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
isRequired: true,
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
try {
await moduleLoader({ container, moduleResolutions, logger })
} catch (err) {
expect(err.message).toEqual(
"No service found in module. Make sure your module exports a service."
)
}
})
it("should throw an error if the default package isn't found and the module is required", async () => {
expect.assertions(1)
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@medusajs/testService",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "@medusajs/testService",
label: "TestService",
isRequired: true,
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
try {
await moduleLoader({ container, moduleResolutions, logger })
} catch (err) {
expect(err.message).toEqual(
`Make sure you have installed the default package: @medusajs/testService`
)
}
})
it("should throw an error if no scope is defined on the module declaration", async () => {
expect.assertions(1)
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/no-service",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
isRequired: true,
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
// @ts-ignore
moduleDeclaration: {
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
try {
await moduleLoader({ container, moduleResolutions, logger })
} catch (err) {
expect(err.message).toEqual(
"The module TestService has to define its scope (internal | external)"
)
}
})
it("should throw an error if the resources is not set when scope is defined as internal", async () => {
expect.assertions(1)
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: "@modules/no-service",
definition: {
registrationName: "testService",
key: "testService",
defaultPackage: "testService",
label: "TestService",
isRequired: true,
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
// @ts-ignore
moduleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
},
} as any,
}
try {
await moduleLoader({ container, moduleResolutions, logger })
} catch (err) {
expect(err.message).toEqual(
"The module TestService is missing its resources config"
)
}
})
})
@@ -0,0 +1,98 @@
import { createMedusaContainer } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix"
import { moduleProviderLoader } from "../module-provider-loader"
const logger = {
warn: jest.fn(),
error: jest.fn(),
} as any
describe("modules loader", () => {
let container
afterEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
container = createMedusaContainer()
})
it("should register the provider service", async () => {
const moduleProviders = [
{
resolve: "@plugins/default",
options: {},
},
]
await moduleProviderLoader({ container, providers: moduleProviders })
const testService = container.resolve("testService")
expect(testService).toBeTruthy()
expect(testService.constructor.name).toEqual("TestService")
})
it("should register the provider service with custom register fn", async () => {
const fn = async (klass, container, details) => {
container.register({
[`testServiceCustomRegistration`]: asFunction(
(cradle) => new klass(cradle, details.options),
{
lifetime: Lifetime.SINGLETON,
}
),
})
}
const moduleProviders = [
{
resolve: "@plugins/default",
options: {},
},
]
await moduleProviderLoader({
container,
providers: moduleProviders,
registerServiceFn: fn,
})
const testService = container.resolve("testServiceCustomRegistration")
expect(testService).toBeTruthy()
expect(testService.constructor.name).toEqual("TestService")
})
it("should log the errors if no service is defined", async () => {
const moduleProviders = [
{
resolve: "@plugins/no-service",
options: {},
},
]
try {
await moduleProviderLoader({ container, providers: moduleProviders })
} catch (error) {
expect(error.message).toBe(
"No services found in plugin @plugins/no-service -- make sure your plugin has a default export of services."
)
}
})
it("should throw if no default export is defined", async () => {
const moduleProviders = [
{
resolve: "@plugins/no-default",
options: {},
},
]
try {
await moduleProviderLoader({ container, providers: moduleProviders })
} catch (error) {
expect(error.message).toBe(
"No services found in plugin @plugins/no-default -- make sure your plugin has a default export of services."
)
}
})
})
@@ -0,0 +1,244 @@
import {
InternalModuleDeclaration,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
ModuleDefinition,
} from "@medusajs/types"
import { ModulesDefinition } from "../../definitions"
import { registerMedusaModule } from "../register-modules"
const RESOLVED_PACKAGE = "@medusajs/test-service-resolved"
jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE))
describe("module definitions loader", () => {
const defaultDefinition: ModuleDefinition = {
key: "testService",
registrationName: "testService",
defaultPackage: "@medusajs/test-service",
label: "TestService",
isLegacy: true,
isRequired: false,
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
}
beforeEach(() => {
jest.resetModules()
jest.clearAllMocks()
// Clear module definitions
const allProperties = Object.getOwnPropertyNames(ModulesDefinition)
allProperties.forEach((property) => {
delete ModulesDefinition[property]
})
})
it("Resolves module with default definition given empty config", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(defaultDefinition.key)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: defaultDefinition.defaultPackage,
definition: defaultDefinition,
options: {},
moduleDeclaration: {
scope: "internal",
resources: "shared",
},
})
)
})
it("Resolves a custom module without pre-defined definition", () => {
const res = registerMedusaModule("customModulesABC", {
options: {
test: 123,
},
})
expect(res).toEqual({
customModulesABC: expect.objectContaining({
resolutionPath: "@medusajs/test-service-resolved",
definition: expect.objectContaining({
key: "customModulesABC",
label: "Custom: customModulesABC",
registrationName: "customModulesABC",
}),
moduleDeclaration: {
resources: "shared",
scope: "internal",
},
options: {
test: 123,
},
}),
})
})
describe("boolean config", () => {
it("Resolves module with no resolution path when given false", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(defaultDefinition.key, false)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: false,
definition: defaultDefinition,
options: {},
})
)
})
it("Fails to resolve module with no resolution path when given false for a required module", () => {
expect.assertions(1)
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: { ...defaultDefinition, isRequired: true },
})
try {
registerMedusaModule(defaultDefinition.key, false)
} catch (err) {
expect(err.message).toEqual(
`Module: ${defaultDefinition.label} is required`
)
}
})
})
it("Module with no resolution path when not given custom resolution path, false as default package and required", () => {
const definition = {
...defaultDefinition,
defaultPackage: false as false,
isRequired: true,
}
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: definition,
})
const res = registerMedusaModule(defaultDefinition.key)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: false,
definition: definition,
options: {},
moduleDeclaration: {
scope: "internal",
resources: "shared",
},
})
)
})
describe("string config", () => {
it("Resolves module with default definition given empty config", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(
defaultDefinition.key,
defaultDefinition.defaultPackage
)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: RESOLVED_PACKAGE,
definition: defaultDefinition,
options: {},
moduleDeclaration: {
scope: "internal",
resources: "shared",
},
})
)
})
})
describe("object config", () => {
it("Resolves resolution path and provides empty options when none are provided", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(defaultDefinition.key, {
scope: MODULE_SCOPE.INTERNAL,
resolve: defaultDefinition.defaultPackage,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
} as InternalModuleDeclaration)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: RESOLVED_PACKAGE,
definition: defaultDefinition,
options: {},
moduleDeclaration: {
scope: "internal",
resources: "isolated",
resolve: defaultDefinition.defaultPackage,
},
})
)
})
it("Resolves default resolution path and provides options when only options are provided", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(defaultDefinition.key, {
options: { test: 123 },
} as any)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: defaultDefinition.defaultPackage,
definition: defaultDefinition,
options: { test: 123 },
moduleDeclaration: {
scope: "internal",
resources: "shared",
options: { test: 123 },
},
})
)
})
it("Resolves resolution path and provides options when only options are provided", () => {
Object.assign(ModulesDefinition, {
[defaultDefinition.key]: defaultDefinition,
})
const res = registerMedusaModule(defaultDefinition.key, {
resolve: defaultDefinition.defaultPackage,
options: { test: 123 },
scope: "internal",
resources: "isolated",
} as any)
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
resolutionPath: RESOLVED_PACKAGE,
definition: defaultDefinition,
options: { test: 123 },
moduleDeclaration: {
scope: "internal",
resources: "isolated",
resolve: defaultDefinition.defaultPackage,
options: { test: 123 },
},
})
)
})
})
})
@@ -0,0 +1,4 @@
export * from "./module-loader"
export * from "./module-provider-loader"
export * from "./register-modules"
@@ -0,0 +1,99 @@
import {
Logger,
MedusaContainer,
MODULE_SCOPE,
ModuleResolution,
} from "@medusajs/types"
import { asValue } from "awilix"
import { EOL } from "os"
import { loadInternalModule } from "./utils"
export const moduleLoader = async ({
container,
moduleResolutions,
logger,
migrationOnly,
loaderOnly,
}: {
container: MedusaContainer
moduleResolutions: Record<string, ModuleResolution>
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
}): Promise<void> => {
for (const resolution of Object.values(moduleResolutions ?? {})) {
const registrationResult = await loadModule(
container,
resolution,
logger!,
migrationOnly,
loaderOnly
)
if (registrationResult?.error) {
const { error } = registrationResult
if (resolution.definition.isRequired) {
logger?.error(
`Could not resolve required module: ${resolution.definition.label}. Error: ${error.message}${EOL}`
)
throw error
}
logger?.warn(
`Could not resolve module: ${resolution.definition.label}. Error: ${error.message}${EOL}`
)
}
}
}
async function loadModule(
container: MedusaContainer,
resolution: ModuleResolution,
logger: Logger,
migrationOnly?: boolean,
loaderOnly?: boolean
): Promise<{ error?: Error } | void> {
const modDefinition = resolution.definition
const registrationName = modDefinition.registrationName
const { scope, resources } = resolution.moduleDeclaration ?? ({} as any)
const canSkip =
!resolution.resolutionPath &&
!modDefinition.isRequired &&
!modDefinition.defaultPackage
if (scope === MODULE_SCOPE.EXTERNAL && !canSkip) {
// TODO: implement external Resolvers
// return loadExternalModule(...)
throw new Error("External Modules are not supported yet.")
}
if (!scope || (scope === MODULE_SCOPE.INTERNAL && !resources)) {
let message = `The module ${resolution.definition.label} has to define its scope (internal | external)`
if (scope === MODULE_SCOPE.INTERNAL && !resources) {
message = `The module ${resolution.definition.label} is missing its resources config`
}
container.register(registrationName, asValue(undefined))
return {
error: new Error(message),
}
}
if (resolution.resolutionPath === false) {
container.register(registrationName, asValue(undefined))
return
}
return await loadInternalModule(
container,
resolution,
logger,
migrationOnly,
loaderOnly
)
}
@@ -0,0 +1,80 @@
import { MedusaContainer, ModuleProvider } from "@medusajs/types"
import { isString, lowerCaseFirst, promiseAll } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix"
export async function moduleProviderLoader({
container,
providers,
registerServiceFn,
}: {
container: MedusaContainer
providers: ModuleProvider[]
registerServiceFn?: (
klass,
container: MedusaContainer,
pluginDetails: any
) => Promise<void>
}) {
if (!providers?.length) {
return
}
await promiseAll(
providers.map(async (pluginDetails) => {
await loadModuleProvider(container, pluginDetails, registerServiceFn)
})
)
}
export async function loadModuleProvider(
container: MedusaContainer,
provider: ModuleProvider,
registerServiceFn?: (klass, container, pluginDetails) => Promise<void>
) {
let loadedProvider: any
const pluginName = provider.resolve ?? provider.provider_name ?? ""
try {
loadedProvider = provider.resolve
if (isString(provider.resolve)) {
loadedProvider = await import(provider.resolve)
}
} catch (error) {
throw new Error(
`Unable to find plugin ${pluginName} -- perhaps you need to install its package?`
)
}
loadedProvider = (loadedProvider as any).default ?? loadedProvider
if (!loadedProvider?.services?.length) {
throw new Error(
`No services found in plugin ${provider.resolve} -- make sure your plugin has a default export of services.`
)
}
const services = await promiseAll(
loadedProvider.services.map(async (service) => {
const name = lowerCaseFirst(service.name)
if (registerServiceFn) {
// Used to register the specific type of service in the provider
await registerServiceFn(service, container, provider.options)
} else {
container.register({
[name]: asFunction(
(cradle) => new service(cradle, provider.options),
{
lifetime: service.LIFE_TIME || Lifetime.SCOPED,
}
),
})
}
return service
})
)
return services
}
@@ -0,0 +1,165 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
ModuleDefinition,
ModuleExports,
ModuleResolution,
} from "@medusajs/types"
import { isObject, isString } from "@medusajs/utils"
import resolveCwd from "resolve-cwd"
import { ModulesDefinition } from "../definitions"
export const registerMedusaModule = (
moduleKey: string,
moduleDeclaration?:
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
| string
| false,
moduleExports?: ModuleExports,
definition?: ModuleDefinition
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
const modDefinition = definition ?? ModulesDefinition[moduleKey]
const modDeclaration =
moduleDeclaration ??
(modDefinition?.defaultModuleDeclaration as InternalModuleDeclaration)
if (modDeclaration !== false && !modDeclaration) {
throw new Error(`Module: ${moduleKey} has no declaration.`)
}
if (
isObject(modDeclaration) &&
modDeclaration?.scope === MODULE_SCOPE.EXTERNAL
) {
// TODO: getExternalModuleResolution(...)
throw new Error("External Modules are not supported yet.")
}
if (modDefinition === undefined) {
moduleResolutions[moduleKey] = getCustomModuleResolution(
moduleKey,
moduleDeclaration as InternalModuleDeclaration
)
return moduleResolutions
}
moduleResolutions[moduleKey] = getInternalModuleResolution(
modDefinition,
moduleDeclaration as InternalModuleDeclaration,
moduleExports
)
return moduleResolutions
}
function getCustomModuleResolution(
key: string,
moduleConfig: InternalModuleDeclaration | string
): ModuleResolution {
const resolutionPath = resolveCwd(
isString(moduleConfig) ? moduleConfig : (moduleConfig.resolve as string)
)
const conf = isObject(moduleConfig)
? moduleConfig
: ({} as InternalModuleDeclaration)
const dependencies = conf?.dependencies ?? []
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: conf?.resources ?? MODULE_RESOURCE_TYPE.SHARED,
scope: MODULE_SCOPE.INTERNAL,
},
dependencies,
options: conf?.options ?? {},
}
}
export const registerMedusaLinkModule = (
definition: ModuleDefinition,
moduleDeclaration: Partial<InternalModuleDeclaration>,
moduleExports?: ModuleExports
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
moduleDeclaration as InternalModuleDeclaration,
moduleExports
)
return moduleResolutions
}
function getInternalModuleResolution(
definition: ModuleDefinition,
moduleConfig: InternalModuleDeclaration | string | false,
moduleExports?: ModuleExports
): ModuleResolution {
if (typeof moduleConfig === "boolean") {
if (!moduleConfig && definition.isRequired) {
throw new Error(`Module: ${definition.label} is required`)
}
if (!moduleConfig) {
return {
resolutionPath: false,
definition,
dependencies: [],
options: {},
}
}
}
const isObj = isObject(moduleConfig)
let resolutionPath = definition.defaultPackage
// If user added a module and it's overridable, we resolve that instead
const isStr = isString(moduleConfig)
if (isStr || (isObj && moduleConfig.resolve)) {
resolutionPath = !moduleExports
? resolveCwd(isStr ? moduleConfig : (moduleConfig.resolve as string))
: // Explicitly assign an empty string, later, we will check if the value is exactly false.
// This allows to continue the module loading while using the module exports instead of re importing the module itself during the process.
""
}
const moduleDeclaration = isObj ? moduleConfig : {}
const additionalDependencies = isObj ? moduleConfig.dependencies || [] : []
return {
resolutionPath,
definition,
dependencies: [
...new Set(
(definition.dependencies || []).concat(additionalDependencies)
),
],
moduleDeclaration: {
...(definition.defaultModuleDeclaration ?? {}),
...moduleDeclaration,
},
moduleExports,
options: isObj ? moduleConfig.options ?? {} : {},
}
}
@@ -0,0 +1 @@
export * from "./load-internal"
@@ -0,0 +1,162 @@
import {
InternalModuleDeclaration,
Logger,
MedusaContainer,
MODULE_RESOURCE_TYPE,
ModuleExports,
ModuleResolution,
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
createMedusaContainer,
MedusaModuleType,
} from "@medusajs/utils"
import { asFunction, asValue } from "awilix"
export async function loadInternalModule(
container: MedusaContainer,
resolution: ModuleResolution,
logger: Logger,
migrationOnly?: boolean,
loaderOnly?: boolean
): Promise<{ error?: Error } | void> {
const registrationName = !loaderOnly
? resolution.definition.registrationName
: resolution.definition.registrationName + "__loaderOnly"
const { resources } =
resolution.moduleDeclaration as InternalModuleDeclaration
let loadedModule: ModuleExports
try {
// When loading manually, we pass the exports to be loaded, meaning that we do not need to import the package to find
// the exports. This is useful when a package export an initialize function which will bootstrap itself and therefore
// does not need to import the package that is currently being loaded as it would create a
// circular reference.
const modulePath = resolution.resolutionPath as string
if (resolution.moduleExports) {
loadedModule = resolution.moduleExports
} else {
loadedModule = await import(modulePath)
loadedModule = (loadedModule as any).default
}
} catch (error) {
if (
resolution.definition.isRequired &&
resolution.definition.defaultPackage
) {
return {
error: new Error(
`Make sure you have installed the default package: ${resolution.definition.defaultPackage}`
),
}
}
return { error }
}
if (!loadedModule?.service) {
container.register({
[registrationName]: asValue(undefined),
})
return {
error: new Error(
"No service found in module. Make sure your module exports a service."
),
}
}
if (migrationOnly) {
// Partially loaded module, only register the service __joinerConfig function to be able to resolve it later
const moduleService = {
__joinerConfig: loadedModule.service.prototype.__joinerConfig,
}
container.register({
[registrationName]: asValue(moduleService),
})
return
}
const localContainer = createMedusaContainer()
const dependencies = resolution?.dependencies ?? []
if (resources === MODULE_RESOURCE_TYPE.SHARED) {
dependencies.push(
ContainerRegistrationKeys.MANAGER,
ContainerRegistrationKeys.CONFIG_MODULE,
ContainerRegistrationKeys.LOGGER,
ContainerRegistrationKeys.PG_CONNECTION
)
}
for (const dependency of dependencies) {
localContainer.register(
dependency,
asFunction(() => {
return container.resolve(dependency, { allowUnregistered: true })
})
)
}
const moduleLoaders = loadedModule?.loaders ?? []
try {
for (const loader of moduleLoaders) {
await loader(
{
container: localContainer,
logger,
options: resolution.options,
dataLoaderOnly: loaderOnly,
},
resolution.moduleDeclaration as InternalModuleDeclaration
)
}
} catch (err) {
container.register({
[registrationName]: asValue(undefined),
})
return {
error: new Error(
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
),
}
}
const moduleService = loadedModule.service
container.register({
[registrationName]: asFunction((cradle) => {
;(moduleService as any).__type = MedusaModuleType
return new moduleService(
localContainer.cradle,
resolution.options,
resolution.moduleDeclaration
)
}).singleton(),
})
if (loaderOnly) {
// The expectation is only to run the loader as standalone, so we do not need to register the service and we need to cleanup all services
const service = container.resolve(registrationName)
await service.__hooks?.onApplicationPrepareShutdown()
await service.__hooks?.onApplicationShutdown()
}
}
export async function loadModuleMigrations(
resolution: ModuleResolution,
moduleExports?: ModuleExports
): Promise<[Function | undefined, Function | undefined]> {
let loadedModule: ModuleExports
try {
loadedModule =
moduleExports ?? (await import(resolution.resolutionPath as string))
return [loadedModule.runMigrations, loadedModule.revertMigration]
} catch {
return [undefined, undefined]
}
}