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

View File

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

View File

@@ -444,5 +444,162 @@ describe("load internal", () => {
expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider) expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider)
expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider2) 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 normalizedPath: string
} }
type LoadInternalArgs = {
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
schemaOnly?: boolean
loaderOnly?: boolean
}
type MigrationFunction = ( type MigrationFunction = (
options: LoaderOptions<any>, options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
@@ -115,16 +124,10 @@ export async function resolveModuleExports({
} }
async function loadInternalProvider( async function loadInternalProvider(
args: { args: LoadInternalArgs,
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
},
providers: ModuleProvider[] providers: ModuleProvider[]
): Promise<{ error?: Error } | void> { ): Promise<{ error?: Error } | void> {
const { container, resolution, logger, migrationOnly } = args const { container, resolution, logger, migrationOnly, schemaOnly } = args
const errors: { error?: Error }[] = [] const errors: { error?: Error }[] = []
for (const provider of providers) { for (const provider of providers) {
@@ -154,6 +157,7 @@ async function loadInternalProvider(
}, },
logger, logger,
migrationOnly, migrationOnly,
schemaOnly,
loadingProviders: true, loadingProviders: true,
}) })
@@ -181,6 +185,7 @@ export async function loadInternalModule(args: {
migrationOnly?: boolean migrationOnly?: boolean
loaderOnly?: boolean loaderOnly?: boolean
loadingProviders?: boolean loadingProviders?: boolean
schemaOnly?: boolean
}): Promise<{ error?: Error } | void> { }): Promise<{ error?: Error } | void> {
const { const {
container, container,
@@ -189,6 +194,7 @@ export async function loadInternalModule(args: {
migrationOnly, migrationOnly,
loaderOnly, loaderOnly,
loadingProviders, loadingProviders,
schemaOnly,
} = args } = args
const keyName = !loaderOnly const keyName = !loaderOnly
@@ -283,7 +289,10 @@ export async function loadInternalModule(args: {
)?.options )?.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_ = const moduleService_ =
moduleResources.moduleService ?? loadedModule_.service moduleResources.moduleService ?? loadedModule_.service
@@ -299,6 +308,12 @@ export async function loadInternalModule(args: {
return 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 loaders = moduleResources.loaders ?? loadedModule?.loaders ?? []
const error = await runLoaders(loaders, { const error = await runLoaders(loaders, {
container, container,

View File

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

View File

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

View File

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

View File

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