fix: Add schema only flag on Medusa app loader (#14502)

* fix(build): Introduce a schema only flag for more heavy light weight loading

* fix(build): cleanup and tests

* Create shy-snails-raise.md

* fix(build): cleanup
This commit is contained in:
Adrien de Peretti
2026-01-09 12:21:28 +01:00
committed by GitHub
parent 66f8fe084c
commit 233ec261be
9 changed files with 243 additions and 20 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/medusa": patch
"@medusajs/link-modules": patch
"@medusajs/framework": patch
"@medusajs/modules-sdk": patch
---
Fix/add schema only flag

View File

@@ -311,8 +311,13 @@ export class MedusaAppLoader {
* @param config
*/
async load(
config: { registerInContainer?: boolean; migrationOnly?: boolean } = {
config: {
registerInContainer?: boolean
schemaOnly?: boolean
migrationOnly?: boolean
} = {
registerInContainer: true,
schemaOnly: false,
migrationOnly: false,
}
): Promise<MedusaAppOutput> {
@@ -321,7 +326,9 @@ export class MedusaAppLoader {
)
const { sharedResourcesConfig, injectedDependencies } =
!config.migrationOnly ? this.prepareSharedResourcesAndDeps() : {}
!config.migrationOnly && !config.schemaOnly
? this.prepareSharedResourcesAndDeps()
: {}
this.#container.register(
ContainerRegistrationKeys.REMOTE_QUERY,
@@ -349,6 +356,7 @@ export class MedusaAppLoader {
medusaConfigPath: this.#medusaConfigPath,
cwd: this.#cwd,
migrationOnly: config.migrationOnly,
schemaOnly: config.schemaOnly,
})
if (!config.registerInContainer) {

View File

@@ -11,17 +11,26 @@ export const moduleLoader = async ({
logger,
migrationOnly,
loaderOnly,
schemaOnly,
}: {
container: MedusaContainer
moduleResolutions: Record<string, ModuleResolution>
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
schemaOnly?: boolean
}): Promise<void> => {
const resolutions = Object.values(moduleResolutions ?? {})
const results = await promiseAll(
resolutions.map((resolution) =>
loadModule(container, resolution, logger!, migrationOnly, loaderOnly)
loadModule(
container,
resolution,
logger!,
migrationOnly,
loaderOnly,
schemaOnly
)
)
)
@@ -41,7 +50,8 @@ async function loadModule(
resolution: ModuleResolution,
logger: Logger,
migrationOnly?: boolean,
loaderOnly?: boolean
loaderOnly?: boolean,
schemaOnly?: boolean
): Promise<{ error?: Error } | void> {
const modDefinition = resolution.definition
@@ -85,5 +95,6 @@ async function loadModule(
logger,
migrationOnly,
loaderOnly,
schemaOnly,
})
}

View File

@@ -444,5 +444,162 @@ describe("load internal", () => {
expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider)
expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider2)
})
describe("schemaOnly mode", () => {
test("should only register __joinerConfig when schemaOnly is true", async () => {
const { ModuleService } = ModuleWithJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-joiner-config"
),
moduleDeclaration: {
scope: "internal",
},
definition: {
key: "module-schema-only-test",
label: "Module schema only test",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
},
},
}
const container = createMedusaContainer()
await loadInternalModule({
container: container,
resolution: moduleResolution,
logger: console as any,
schemaOnly: true,
})
const registeredModule = container.resolve<{
__joinerConfig?: () => Record<string, unknown>
}>(moduleResolution.definition.key)
// Should NOT be an instance of the full ModuleService
expect(registeredModule).not.toBeInstanceOf(ModuleService)
// Should have __joinerConfig registered
expect(registeredModule).toHaveProperty("__joinerConfig")
expect(typeof registeredModule.__joinerConfig).toBe("function")
// __joinerConfig should return the expected config
const joinerConfig = registeredModule.__joinerConfig!()
expect(joinerConfig).toEqual(
expect.objectContaining({
serviceName: "module-service",
primaryKeys: ["id"],
})
)
})
test("should not instantiate the full module service when schemaOnly is true", async () => {
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-providers"
),
moduleDeclaration: {
scope: "internal",
},
definition: {
key: "module-schema-only-no-instance",
label: "Module schema only no instance",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
},
},
options: {
providers: [
{
resolve: join(
__dirname,
"../__fixtures__/module-with-providers/provider-1"
),
id: "provider-schema-only",
options: {
api_key: "test",
},
},
],
},
}
const container = createMedusaContainer()
await loadInternalModule({
container: container,
resolution: moduleResolution,
logger: console as any,
schemaOnly: true,
})
const registeredModule = container.resolve(
moduleResolution.definition.key
)
// Should NOT be an instance of the full ModuleService
expect(registeredModule).not.toBeInstanceOf(ModuleServiceWithProvider)
// Should only have __joinerConfig
expect(registeredModule).toHaveProperty("__joinerConfig")
// Should NOT have other service methods/properties
expect(registeredModule).not.toHaveProperty("container")
})
test("should not register providers when schemaOnly is true", async () => {
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-providers"
),
moduleDeclaration: {
scope: "internal",
},
definition: {
key: "module-schema-only-no-providers",
label: "Module schema only no providers",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
},
},
options: {
providers: [
{
resolve: join(
__dirname,
"../__fixtures__/module-with-providers/provider-1"
),
id: "provider-schema-only-test",
options: {
api_key: "test",
},
},
],
},
}
const container = createMedusaContainer()
await loadInternalModule({
container: container,
resolution: moduleResolution,
logger: console as any,
schemaOnly: true,
})
// Provider should NOT be registered in the container
const providerKey = getProviderRegistrationKey({
providerId: "provider-schema-only-test",
providerIdentifier: ModuleServiceWithProviderProvider1.identifier,
})
expect(container.hasRegistration(providerKey)).toBe(false)
})
})
})
})

View File

@@ -45,6 +45,15 @@ type ModuleResource = {
normalizedPath: string
}
type LoadInternalArgs = {
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
schemaOnly?: boolean
loaderOnly?: boolean
}
type MigrationFunction = (
options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration
@@ -115,16 +124,10 @@ export async function resolveModuleExports({
}
async function loadInternalProvider(
args: {
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
},
args: LoadInternalArgs,
providers: ModuleProvider[]
): Promise<{ error?: Error } | void> {
const { container, resolution, logger, migrationOnly } = args
const { container, resolution, logger, migrationOnly, schemaOnly } = args
const errors: { error?: Error }[] = []
for (const provider of providers) {
@@ -154,6 +157,7 @@ async function loadInternalProvider(
},
logger,
migrationOnly,
schemaOnly,
loadingProviders: true,
})
@@ -181,6 +185,7 @@ export async function loadInternalModule(args: {
migrationOnly?: boolean
loaderOnly?: boolean
loadingProviders?: boolean
schemaOnly?: boolean
}): Promise<{ error?: Error } | void> {
const {
container,
@@ -189,6 +194,7 @@ export async function loadInternalModule(args: {
migrationOnly,
loaderOnly,
loadingProviders,
schemaOnly,
} = args
const keyName = !loaderOnly
@@ -283,7 +289,10 @@ export async function loadInternalModule(args: {
)?.options
}
if (migrationOnly && !loadingProviders) {
// Partial module load: register only __joinerConfig
// - migrationOnly: needed for migration planning + loader execution
// - schemaOnly: needed for GraphQL schema + type generation
if ((schemaOnly || migrationOnly) && !loadingProviders) {
const moduleService_ =
moduleResources.moduleService ?? loadedModule_.service
@@ -299,6 +308,12 @@ export async function loadInternalModule(args: {
return
}
if (schemaOnly) {
// in schema only mode, we only need to register the service __joinerConfig function to be able to resolve it later
// For providers in schema-only mode, skip without registration
return
}
const loaders = moduleResources.loaders ?? loadedModule?.loaders ?? []
const error = await runLoaders(loaders, {
container,

View File

@@ -86,6 +86,7 @@ export async function loadModules(args: {
sharedContainer: MedusaContainer
sharedResourcesConfig?: SharedResources
migrationOnly?: boolean
schemaOnly?: boolean
loaderOnly?: boolean
workerMode?: "shared" | "worker" | "server"
cwd?: string
@@ -95,6 +96,7 @@ export async function loadModules(args: {
sharedContainer,
sharedResourcesConfig,
migrationOnly = false,
schemaOnly = false,
loaderOnly = false,
workerMode = "shared" as ModuleBootstrapOptions["workerMode"],
cwd,
@@ -164,6 +166,7 @@ export async function loadModules(args: {
const loaded = (await MedusaModule.bootstrapAll(modulesToLoad, {
migrationOnly,
schemaOnly,
loaderOnly,
workerMode,
cwd,
@@ -205,6 +208,7 @@ async function initializeLinks({
injectedDependencies,
moduleExports,
migrationOnly = false,
schemaOnly = false,
}) {
try {
let resources = moduleExports
@@ -223,7 +227,8 @@ async function initializeLinks({
linkModules,
injectedDependencies,
undefined,
migrationOnly
migrationOnly,
schemaOnly
)
return {
@@ -312,10 +317,15 @@ export type MedusaAppOptions = {
*/
loaderOnly?: boolean
/**
* Only partially load modules to retrieve their joiner configs without running loaders.
* Useful for type generation and migrations.
* Only partially load modules to retrieve their module joiner configs and run their loaders.
* Useful for migrations.
*/
migrationOnly?: boolean
/**
* Only partially load modules to retrieve their module joiner configs without running loaders.
* Useful for type generation.
*/
schemaOnly?: boolean
cwd?: string
}
@@ -330,6 +340,7 @@ async function MedusaApp_({
remoteFetchData,
injectedDependencies = {},
migrationOnly = false,
schemaOnly = false,
loaderOnly = false,
workerMode = "shared",
cwd = process.cwd(),
@@ -428,6 +439,7 @@ async function MedusaApp_({
sharedContainer: sharedContainer_,
sharedResourcesConfig: { database: dbData },
migrationOnly,
schemaOnly,
loaderOnly,
workerMode,
cwd,
@@ -492,6 +504,7 @@ async function MedusaApp_({
injectedDependencies,
moduleExports: isMedusaModule(linkModule) ? linkModule : undefined,
migrationOnly,
schemaOnly,
})
const loadedSchema = getLoadedSchema()

View File

@@ -91,6 +91,7 @@ export type LinkModuleBootstrapOptions = {
injectedDependencies?: Record<string, any>
cwd?: string
migrationOnly?: boolean
schemaOnly?: boolean
}
export type RegisterModuleJoinerConfig =
@@ -301,18 +302,20 @@ class MedusaModule {
public static async bootstrapAll(
modulesOptions: Omit<
ModuleBootstrapOptions,
"migrationOnly" | "loaderOnly" | "workerMode"
"migrationOnly" | "loaderOnly" | "workerMode" | "schemaOnly"
>[],
{
migrationOnly,
loaderOnly,
workerMode,
schemaOnly,
cwd,
}: {
migrationOnly?: boolean
loaderOnly?: boolean
workerMode?: ModuleBootstrapOptions["workerMode"]
cwd?: string
schemaOnly?: boolean
}
): Promise<
{
@@ -324,6 +327,7 @@ class MedusaModule {
loaderOnly,
workerMode,
cwd,
schemaOnly,
})
}
@@ -392,18 +396,20 @@ class MedusaModule {
protected static async bootstrap_<T>(
modulesOptions: Omit<
ModuleBootstrapOptions,
"migrationOnly" | "loaderOnly" | "workerMode" | "cwd"
"migrationOnly" | "loaderOnly" | "workerMode" | "cwd" | "schemaOnly"
>[],
{
migrationOnly,
loaderOnly,
workerMode,
cwd = process.cwd(),
schemaOnly,
}: {
migrationOnly?: boolean
loaderOnly?: boolean
workerMode?: "shared" | "worker" | "server"
cwd?: string
schemaOnly?: boolean
}
): Promise<
{
@@ -508,6 +514,7 @@ class MedusaModule {
moduleResolutions,
logger: logger_,
migrationOnly,
schemaOnly,
loaderOnly,
})
} catch (err) {
@@ -654,6 +661,7 @@ class MedusaModule {
injectedDependencies,
cwd,
migrationOnly,
schemaOnly,
}: LinkModuleBootstrapOptions): Promise<{
[key: string]: unknown
}> {
@@ -723,6 +731,7 @@ class MedusaModule {
container,
moduleResolutions,
migrationOnly,
schemaOnly,
logger: logger_,
})
} catch (err) {

View File

@@ -41,7 +41,7 @@ export async function generateTypes({
const { gqlSchema, modules } = await new MedusaAppLoader().load({
registerInContainer: false,
migrationOnly: true,
schemaOnly: true,
})
const typesDirectory = path.join(directory, ".medusa/types")

View File

@@ -35,7 +35,8 @@ export const initialize = async (
pluginLinksDefinitions?: ModuleJoinerConfig[],
injectedDependencies?: InitializeModuleInjectableDependencies,
cwd?: string,
migrationOnly?: boolean
migrationOnly?: boolean,
schemaOnly?: boolean
): Promise<{ [link: string]: ILinkModule }> => {
const allLinks = {}
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
@@ -172,6 +173,7 @@ export const initialize = async (
injectedDependencies,
cwd,
migrationOnly,
schemaOnly,
})
allLinks[serviceKey as string] = Object.values(loaded)[0]