From 233ec261be200fac83002415aa7c0df082339a3f Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 9 Jan 2026 12:21:28 +0100 Subject: [PATCH] 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 --- .changeset/shy-snails-raise.md | 8 + .../core/framework/src/medusa-app-loader.ts | 12 +- .../modules-sdk/src/loaders/module-loader.ts | 15 +- .../utils/__tests__/load-internal.spec.ts | 157 ++++++++++++++++++ .../src/loaders/utils/load-internal.ts | 33 +++- packages/core/modules-sdk/src/medusa-app.ts | 19 ++- .../core/modules-sdk/src/medusa-module.ts | 13 +- .../src/commands/utils/generate-types.ts | 2 +- .../link-modules/src/initialize/index.ts | 4 +- 9 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 .changeset/shy-snails-raise.md diff --git a/.changeset/shy-snails-raise.md b/.changeset/shy-snails-raise.md new file mode 100644 index 0000000000..67f0af018a --- /dev/null +++ b/.changeset/shy-snails-raise.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": patch +"@medusajs/link-modules": patch +"@medusajs/framework": patch +"@medusajs/modules-sdk": patch +--- + +Fix/add schema only flag diff --git a/packages/core/framework/src/medusa-app-loader.ts b/packages/core/framework/src/medusa-app-loader.ts index 37fef781cc..e27911d141 100644 --- a/packages/core/framework/src/medusa-app-loader.ts +++ b/packages/core/framework/src/medusa-app-loader.ts @@ -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 { @@ -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) { diff --git a/packages/core/modules-sdk/src/loaders/module-loader.ts b/packages/core/modules-sdk/src/loaders/module-loader.ts index 5e12a856aa..0626a9cb40 100644 --- a/packages/core/modules-sdk/src/loaders/module-loader.ts +++ b/packages/core/modules-sdk/src/loaders/module-loader.ts @@ -11,17 +11,26 @@ export const moduleLoader = async ({ logger, migrationOnly, loaderOnly, + schemaOnly, }: { container: MedusaContainer moduleResolutions: Record logger: Logger migrationOnly?: boolean loaderOnly?: boolean + schemaOnly?: boolean }): Promise => { 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, }) } diff --git a/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts b/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts index a3d81a657c..06173b65e5 100644 --- a/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts +++ b/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts @@ -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 + }>(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) + }) + }) }) }) diff --git a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts index 4ea7dd197e..9ecd339be0 100644 --- a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts @@ -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, 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, diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 1123aefb8e..ac6346b1d3 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -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() diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index d24317bd10..2b288a281c 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -91,6 +91,7 @@ export type LinkModuleBootstrapOptions = { injectedDependencies?: Record 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_( 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) { diff --git a/packages/medusa/src/commands/utils/generate-types.ts b/packages/medusa/src/commands/utils/generate-types.ts index 0de904c380..dd55cd8638 100644 --- a/packages/medusa/src/commands/utils/generate-types.ts +++ b/packages/medusa/src/commands/utils/generate-types.ts @@ -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") diff --git a/packages/modules/link-modules/src/initialize/index.ts b/packages/modules/link-modules/src/initialize/index.ts index 6990bcd453..a3d0cfce00 100644 --- a/packages/modules/link-modules/src/initialize/index.ts +++ b/packages/modules/link-modules/src/initialize/index.ts @@ -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]