feat: generate modules mappings at runtime (#10791)

This commit is contained in:
Harminder Virk
2025-01-03 15:49:47 +05:30
committed by GitHub
parent 5e9d86d75d
commit ecc09fd77d
9 changed files with 220 additions and 12 deletions

View File

@@ -332,8 +332,8 @@ async function MedusaApp_({
modulesConfig ??
(
await dynamicImport(
await (modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config"))
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default

View File

@@ -559,6 +559,11 @@ class MedusaModule {
services[keyName] = container.resolve(keyName)
services[keyName].__definition = resolution.definition
services[keyName].__definition.resolvePath =
"resolve" in modDeclaration &&
typeof modDeclaration.resolve === "string"
? modDeclaration.resolve
: undefined
if (resolution.definition.isQueryable) {
let joinerConfig!: ModuleJoinerConfig

View File

@@ -93,6 +93,7 @@ export type ModuleDefinition = {
key: string
defaultPackage: string | false
label: string
resolvePath?: string
isRequired?: boolean
isQueryable?: boolean // If the module is queryable via Remote Joiner
dependencies?: string[]

View File

@@ -0,0 +1,81 @@
import { join } from "path"
import { FileSystem } from "../../common"
import { generateContainerTypes } from "../modules-to-container-types"
const fileSystem = new FileSystem(join(__dirname, "./tmp"))
afterEach(async () => {
await fileSystem.cleanup()
})
describe("generateContainerTypes", function () {
it("should create file with types for provided modules", async function () {
await generateContainerTypes(
{
cache: {
__definition: {
key: "cache",
label: "Cache",
defaultPackage: "@medusajs/foo",
resolvePath: "@medusajs/foo",
defaultModuleDeclaration: {
scope: "internal",
},
},
__joinerConfig: {},
},
},
{
outputDir: fileSystem.basePath,
interfaceName: "ModulesImplementations",
}
)
expect(await fileSystem.exists("modules-bindings.d.ts")).toBeTruthy()
expect(await fileSystem.contents("modules-bindings.d.ts"))
.toMatchInlineSnapshot(`
"import type Cache from '@medusajs/foo'
declare module '@medusajs/framework/types' {
interface ModulesImplementations {
cache: InstanceType<(typeof Cache)['service']>
}
}"
`)
})
it("should normalize module path pointing to a relative file", async function () {
await generateContainerTypes(
{
cache: {
__definition: {
key: "cache",
label: "Cache",
defaultPackage: "./foo/bar",
resolvePath: "./foo/bar",
defaultModuleDeclaration: {
scope: "internal",
},
},
__joinerConfig: {},
},
},
{
outputDir: fileSystem.basePath,
interfaceName: "ModulesImplementations",
}
)
expect(await fileSystem.exists("modules-bindings.d.ts")).toBeTruthy()
expect(await fileSystem.contents("modules-bindings.d.ts"))
.toMatchInlineSnapshot(`
"import type Cache from '../../foo/bar'
declare module '@medusajs/framework/types' {
interface ModulesImplementations {
cache: InstanceType<(typeof Cache)['service']>
}
}"
`)
})
})

View File

@@ -20,3 +20,4 @@ export * from "./query-context"
export * from "./types/links-config"
export * from "./types/medusa-service"
export * from "./module-provider-registration-key"
export * from "./modules-to-container-types"

View File

@@ -0,0 +1,99 @@
import { join } from "path"
import type { LoadedModule } from "@medusajs/types"
import { FileSystem } from "../common/file-system"
import { toCamelCase } from "../common/to-camel-case"
import { upperCaseFirst } from "../common/upper-case-first"
/**
* Modules registered inside the config file points to one
* of the following paths.
*
* - A package name
* - A relative application import
* - Or an absolute path using `require.resolve`
*
* In case of a relative import, we mutate the path to resolve properly
* when the output file is inside the ".medusa/types" directory.
* For example:
*
* => "./src/modules/brand" will become "../../src/modules/brand"
*
* Package names and absolute paths are left as it is.
*/
function normalizeModuleResolvePath(modulePath: string) {
return modulePath.startsWith("./") || modulePath.startsWith("../")
? join("../", "../", modulePath)
: modulePath
}
/**
* Creates the "modules-bindings.d.ts" file with container mappings
* for the modules enabled inside a user's project.
*/
export async function generateContainerTypes(
modules: Record<string, LoadedModule | LoadedModule[]>,
{
outputDir,
interfaceName,
}: {
outputDir: string
interfaceName: string
}
) {
const { imports, mappings } = Object.keys(modules).reduce(
(result, key) => {
const services = Array.isArray(modules[key])
? modules[key]
: [modules[key]]
services.forEach((service) => {
if (!service.__definition.resolvePath) {
return
}
/**
* Key registered within the container
*/
const key = service.__definition.key
/**
* @todo. The property should exist on "LoadedModule"
*/
let servicePath: string = normalizeModuleResolvePath(
service.__definition.resolvePath
)
/**
* We create the service name (aka default import name) from the
* service key that is registered inside the container.
*/
const serviceName = upperCaseFirst(toCamelCase(key))
result.imports.push(`import type ${serviceName} from '${servicePath}'`)
result.mappings.push(
`${key}: InstanceType<(typeof ${serviceName})['service']>`
)
})
return result
},
{
imports: [],
mappings: [],
} as {
imports: string[]
mappings: string[]
}
)
const fileSystem = new FileSystem(outputDir)
const fileName = "modules-bindings.d.ts"
const fileContents = `${imports.join(
"\n"
)}\n\ndeclare module '@medusajs/framework/types' {
interface ${interfaceName} {
${mappings.join(",\n ")}
}
}`
await fileSystem.create(fileName, fileContents)
}