Skip to content

Commit

Permalink
feat: generate modules mappings at runtime (#10791)
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage authored Jan 3, 2025
1 parent 5e9d86d commit ecc09fd
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 12 deletions.
8 changes: 8 additions & 0 deletions .changeset/angry-coats-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@medusajs/medusa": patch
"@medusajs/modules-sdk": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---

feat: generate modules mappings at runtime
4 changes: 2 additions & 2 deletions packages/core/modules-sdk/src/medusa-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ async function MedusaApp_({
modulesConfig ??
(
await dynamicImport(
await (modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config"))
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default

Expand Down
5 changes: 5 additions & 0 deletions packages/core/modules-sdk/src/medusa-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/src/modules-sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -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']>
}
}"
`)
})
})
1 change: 1 addition & 0 deletions packages/core/utils/src/modules-sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
99 changes: 99 additions & 0 deletions packages/core/utils/src/modules-sdk/modules-to-container-types.ts
Original file line number Diff line number Diff line change
@@ -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)
}
29 changes: 19 additions & 10 deletions packages/medusa/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
gqlSchemaToTypes,
GracefulShutdownServer,
isPresent,
generateContainerTypes,
} from "@medusajs/framework/utils"
import { logger } from "@medusajs/framework/logger"

Expand Down Expand Up @@ -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`)
Expand Down
4 changes: 4 additions & 0 deletions packages/medusa/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ConfigModule,
LoadedModule,
MedusaContainer,
PluginDetails,
} from "@medusajs/framework/types"
Expand Down Expand Up @@ -136,6 +137,7 @@ export default async ({
}: Options): Promise<{
container: MedusaContainer
app: Express
modules: Record<string, LoadedModule | LoadedModule[]>
shutdown: () => Promise<void>
gqlSchema?: GraphQLSchema
}> => {
Expand All @@ -154,6 +156,7 @@ export default async ({
onApplicationStart,
onApplicationShutdown,
onApplicationPrepareShutdown,
modules,
gqlSchema,
} = await new MedusaAppLoader().load()

Expand Down Expand Up @@ -192,6 +195,7 @@ export default async ({
container,
app: expressApp,
shutdown,
modules,
gqlSchema,
}
}

0 comments on commit ecc09fd

Please sign in to comment.