diff --git a/.changeset/angry-coats-warn.md b/.changeset/angry-coats-warn.md new file mode 100644 index 0000000000..39a9b90a29 --- /dev/null +++ b/.changeset/angry-coats-warn.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": patch +"@medusajs/modules-sdk": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat: generate modules mappings at runtime diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 64fdb1b705..05ea45d5f5 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -332,8 +332,8 @@ async function MedusaApp_({ modulesConfig ?? ( await dynamicImport( - await (modulesConfigPath ?? - process.cwd() + (modulesConfigFileName ?? "/modules-config")) + modulesConfigPath ?? + process.cwd() + (modulesConfigFileName ?? "/modules-config") ) ).default diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index 27898e9d72..26db36b71c 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -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 diff --git a/packages/core/types/src/modules-sdk/index.ts b/packages/core/types/src/modules-sdk/index.ts index e1b3d7e7dc..eae2050902 100644 --- a/packages/core/types/src/modules-sdk/index.ts +++ b/packages/core/types/src/modules-sdk/index.ts @@ -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[] diff --git a/packages/core/utils/src/modules-sdk/__tests__/modules-to-container-types.spec.ts b/packages/core/utils/src/modules-sdk/__tests__/modules-to-container-types.spec.ts new file mode 100644 index 0000000000..6ea381163f --- /dev/null +++ b/packages/core/utils/src/modules-sdk/__tests__/modules-to-container-types.spec.ts @@ -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']> + } + }" + `) + }) +}) diff --git a/packages/core/utils/src/modules-sdk/index.ts b/packages/core/utils/src/modules-sdk/index.ts index 1a84d3be30..2d66bc413d 100644 --- a/packages/core/utils/src/modules-sdk/index.ts +++ b/packages/core/utils/src/modules-sdk/index.ts @@ -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" diff --git a/packages/core/utils/src/modules-sdk/modules-to-container-types.ts b/packages/core/utils/src/modules-sdk/modules-to-container-types.ts new file mode 100644 index 0000000000..4cd54d410a --- /dev/null +++ b/packages/core/utils/src/modules-sdk/modules-to-container-types.ts @@ -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, + { + 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) +} diff --git a/packages/medusa/src/commands/start.ts b/packages/medusa/src/commands/start.ts index f5e48f1a2a..a2e2c30ec3 100644 --- a/packages/medusa/src/commands/start.ts +++ b/packages/medusa/src/commands/start.ts @@ -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`) diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index dfc27634cd..bd33ce8b1b 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -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 shutdown: () => Promise 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, } }