feat: generate modules mappings at runtime (#10791)
This commit is contained in:
8
.changeset/angry-coats-warn.md
Normal file
8
.changeset/angry-coats-warn.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/modules-sdk": patch
|
||||
"@medusajs/types": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
feat: generate modules mappings at runtime
|
||||
@@ -332,8 +332,8 @@ async function MedusaApp_({
|
||||
modulesConfig ??
|
||||
(
|
||||
await dynamicImport(
|
||||
await (modulesConfigPath ??
|
||||
process.cwd() + (modulesConfigFileName ?? "/modules-config"))
|
||||
modulesConfigPath ??
|
||||
process.cwd() + (modulesConfigFileName ?? "/modules-config")
|
||||
)
|
||||
).default
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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']>
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
gqlSchemaToTypes,
|
||||
GracefulShutdownServer,
|
||||
isPresent,
|
||||
generateContainerTypes,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { logger } from "@medusajs/framework/logger"
|
||||
|
||||
@@ -120,21 +121,29 @@ async function start(args: {
|
||||
})
|
||||
|
||||
try {
|
||||
const { shutdown, gqlSchema, container } = await loaders({
|
||||
const { shutdown, gqlSchema, container, modules } = await loaders({
|
||||
directory,
|
||||
expressApp: app,
|
||||
})
|
||||
|
||||
if (gqlSchema && generateTypes) {
|
||||
const outputDirGeneratedTypes = path.join(directory, ".medusa/types")
|
||||
await gqlSchemaToTypes({
|
||||
outputDir: outputDirGeneratedTypes,
|
||||
filename: "remote-query-entry-points",
|
||||
interfaceName: "RemoteQueryEntryPoints",
|
||||
schema: gqlSchema,
|
||||
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
|
||||
if (generateTypes) {
|
||||
await generateContainerTypes(modules, {
|
||||
outputDir: path.join(directory, ".medusa/types"),
|
||||
interfaceName: "ModuleImplementations",
|
||||
})
|
||||
logger.info("Generated modules types")
|
||||
logger.debug("Generated container types")
|
||||
|
||||
if (gqlSchema) {
|
||||
const outputDirGeneratedTypes = path.join(directory, ".medusa/types")
|
||||
await gqlSchemaToTypes({
|
||||
outputDir: outputDirGeneratedTypes,
|
||||
filename: "remote-query-entry-points",
|
||||
interfaceName: "RemoteQueryEntryPoints",
|
||||
schema: gqlSchema,
|
||||
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
|
||||
})
|
||||
logger.debug("Generated modules types")
|
||||
}
|
||||
}
|
||||
|
||||
const serverActivity = logger.activity(`Creating server`)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ConfigModule,
|
||||
LoadedModule,
|
||||
MedusaContainer,
|
||||
PluginDetails,
|
||||
} from "@medusajs/framework/types"
|
||||
@@ -136,6 +137,7 @@ export default async ({
|
||||
}: Options): Promise<{
|
||||
container: MedusaContainer
|
||||
app: Express
|
||||
modules: Record<string, LoadedModule | LoadedModule[]>
|
||||
shutdown: () => Promise<void>
|
||||
gqlSchema?: GraphQLSchema
|
||||
}> => {
|
||||
@@ -154,6 +156,7 @@ export default async ({
|
||||
onApplicationStart,
|
||||
onApplicationShutdown,
|
||||
onApplicationPrepareShutdown,
|
||||
modules,
|
||||
gqlSchema,
|
||||
} = await new MedusaAppLoader().load()
|
||||
|
||||
@@ -192,6 +195,7 @@ export default async ({
|
||||
container,
|
||||
app: expressApp,
|
||||
shutdown,
|
||||
modules,
|
||||
gqlSchema,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user