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 ??
|
modulesConfig ??
|
||||||
(
|
(
|
||||||
await dynamicImport(
|
await dynamicImport(
|
||||||
await (modulesConfigPath ??
|
modulesConfigPath ??
|
||||||
process.cwd() + (modulesConfigFileName ?? "/modules-config"))
|
process.cwd() + (modulesConfigFileName ?? "/modules-config")
|
||||||
)
|
)
|
||||||
).default
|
).default
|
||||||
|
|
||||||
|
|||||||
@@ -559,6 +559,11 @@ class MedusaModule {
|
|||||||
|
|
||||||
services[keyName] = container.resolve(keyName)
|
services[keyName] = container.resolve(keyName)
|
||||||
services[keyName].__definition = resolution.definition
|
services[keyName].__definition = resolution.definition
|
||||||
|
services[keyName].__definition.resolvePath =
|
||||||
|
"resolve" in modDeclaration &&
|
||||||
|
typeof modDeclaration.resolve === "string"
|
||||||
|
? modDeclaration.resolve
|
||||||
|
: undefined
|
||||||
|
|
||||||
if (resolution.definition.isQueryable) {
|
if (resolution.definition.isQueryable) {
|
||||||
let joinerConfig!: ModuleJoinerConfig
|
let joinerConfig!: ModuleJoinerConfig
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export type ModuleDefinition = {
|
|||||||
key: string
|
key: string
|
||||||
defaultPackage: string | false
|
defaultPackage: string | false
|
||||||
label: string
|
label: string
|
||||||
|
resolvePath?: string
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
isQueryable?: boolean // If the module is queryable via Remote Joiner
|
isQueryable?: boolean // If the module is queryable via Remote Joiner
|
||||||
dependencies?: string[]
|
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/links-config"
|
||||||
export * from "./types/medusa-service"
|
export * from "./types/medusa-service"
|
||||||
export * from "./module-provider-registration-key"
|
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,
|
gqlSchemaToTypes,
|
||||||
GracefulShutdownServer,
|
GracefulShutdownServer,
|
||||||
isPresent,
|
isPresent,
|
||||||
|
generateContainerTypes,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { logger } from "@medusajs/framework/logger"
|
import { logger } from "@medusajs/framework/logger"
|
||||||
|
|
||||||
@@ -120,21 +121,29 @@ async function start(args: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { shutdown, gqlSchema, container } = await loaders({
|
const { shutdown, gqlSchema, container, modules } = await loaders({
|
||||||
directory,
|
directory,
|
||||||
expressApp: app,
|
expressApp: app,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (gqlSchema && generateTypes) {
|
if (generateTypes) {
|
||||||
const outputDirGeneratedTypes = path.join(directory, ".medusa/types")
|
await generateContainerTypes(modules, {
|
||||||
await gqlSchemaToTypes({
|
outputDir: path.join(directory, ".medusa/types"),
|
||||||
outputDir: outputDirGeneratedTypes,
|
interfaceName: "ModuleImplementations",
|
||||||
filename: "remote-query-entry-points",
|
|
||||||
interfaceName: "RemoteQueryEntryPoints",
|
|
||||||
schema: gqlSchema,
|
|
||||||
joinerConfigs: MedusaModule.getAllJoinerConfigs(),
|
|
||||||
})
|
})
|
||||||
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`)
|
const serverActivity = logger.activity(`Creating server`)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
|
LoadedModule,
|
||||||
MedusaContainer,
|
MedusaContainer,
|
||||||
PluginDetails,
|
PluginDetails,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
@@ -136,6 +137,7 @@ export default async ({
|
|||||||
}: Options): Promise<{
|
}: Options): Promise<{
|
||||||
container: MedusaContainer
|
container: MedusaContainer
|
||||||
app: Express
|
app: Express
|
||||||
|
modules: Record<string, LoadedModule | LoadedModule[]>
|
||||||
shutdown: () => Promise<void>
|
shutdown: () => Promise<void>
|
||||||
gqlSchema?: GraphQLSchema
|
gqlSchema?: GraphQLSchema
|
||||||
}> => {
|
}> => {
|
||||||
@@ -154,6 +156,7 @@ export default async ({
|
|||||||
onApplicationStart,
|
onApplicationStart,
|
||||||
onApplicationShutdown,
|
onApplicationShutdown,
|
||||||
onApplicationPrepareShutdown,
|
onApplicationPrepareShutdown,
|
||||||
|
modules,
|
||||||
gqlSchema,
|
gqlSchema,
|
||||||
} = await new MedusaAppLoader().load()
|
} = await new MedusaAppLoader().load()
|
||||||
|
|
||||||
@@ -192,6 +195,7 @@ export default async ({
|
|||||||
container,
|
container,
|
||||||
app: expressApp,
|
app: expressApp,
|
||||||
shutdown,
|
shutdown,
|
||||||
|
modules,
|
||||||
gqlSchema,
|
gqlSchema,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user