chore(): start moving some packages to the core directory (#7215)
This commit is contained in:
committed by
GitHub
parent
fdee748eed
commit
bbccd6481d
@@ -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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user