From 8890f284705a4843a57a3800820208f593689a2a Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 15 Jan 2026 10:00:15 +0100 Subject: [PATCH] feat(medusa): Prevent build command from throwing on missing config (#14540) **What** Prevent the build command from failing on mising config --- .changeset/kind-rivers-fly.md | 6 +++ .../src/config/__tests__/index.spec.ts | 31 +++++++++++++ packages/core/framework/src/config/config.ts | 46 +++++++++++++------ packages/core/framework/src/config/loader.ts | 11 ++++- packages/medusa/src/commands/build.ts | 1 + packages/medusa/src/loaders/index.ts | 5 +- 6 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 .changeset/kind-rivers-fly.md diff --git a/.changeset/kind-rivers-fly.md b/.changeset/kind-rivers-fly.md new file mode 100644 index 0000000000..0e053ea184 --- /dev/null +++ b/.changeset/kind-rivers-fly.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/framework": patch +--- + +feat(medusa): Prevent build command from throwing on missing config diff --git a/packages/core/framework/src/config/__tests__/index.spec.ts b/packages/core/framework/src/config/__tests__/index.spec.ts index 4020c9b9dd..52f35b4c0e 100644 --- a/packages/core/framework/src/config/__tests__/index.spec.ts +++ b/packages/core/framework/src/config/__tests__/index.spec.ts @@ -38,4 +38,35 @@ describe("configLoader", () => { expect(configModule.projectConfig.databaseName).toBe("foo") expect(configModule.projectConfig.workerMode).toBe("worker") }) + + it("should load config without throwing errors when throwOnError is false", async () => { + await configLoader(entryDirectory, "medusa-config", { + throwOnError: false, + }) + + const configModule = container.resolve( + ContainerRegistrationKeys.CONFIG_MODULE + ) + + expect(configModule).toBeDefined() + expect(configModule.projectConfig).toBeDefined() + }) + + it("should pass throwOnError option through to buildHttpConfig", async () => { + // When throwOnError is false, missing jwtSecret and cookieSecret should not cause errors + await configLoader(entryDirectory, "medusa-config-2", { + throwOnError: false, + }) + + const configModule = container.resolve( + ContainerRegistrationKeys.CONFIG_MODULE + ) + + expect(configModule).toBeDefined() + expect(configModule.projectConfig.databaseName).toBe("foo") + // http config should still be built with defaults even without throwing errors + expect(configModule.projectConfig.http).toBeDefined() + expect(configModule.projectConfig.http.jwtSecret).toBe("supersecret") + expect(configModule.projectConfig.http.cookieSecret).toBe("supersecret") + }) }) diff --git a/packages/core/framework/src/config/config.ts b/packages/core/framework/src/config/config.ts index 07fbf7052d..8173ab6997 100644 --- a/packages/core/framework/src/config/config.ts +++ b/packages/core/framework/src/config/config.ts @@ -70,8 +70,12 @@ export class ConfigManager { * @protected */ protected buildHttpConfig( - projectConfig: Partial + projectConfig: Partial, + options?: { + throwOnError?: boolean + } ): ConfigModule["projectConfig"]["http"] { + const { throwOnError = true } = options ?? {} const http = (projectConfig.http ?? {}) as ConfigModule["projectConfig"]["http"] @@ -87,6 +91,7 @@ export class ConfigManager { http.jwtPublicKey = http?.jwtPublicKey ?? process.env.JWT_PUBLIC_KEY if ( + throwOnError && http?.jwtPublicKey && ((http.jwtVerifyOptions && !http.jwtVerifyOptions.algorithms?.length) || (http.jwtOptions && !http.jwtOptions.algorithm)) @@ -97,11 +102,13 @@ export class ConfigManager { } if (!http.jwtSecret) { - this.rejectErrors( - `http.jwtSecret not found.${ - this.#isProduction ? "" : "Using default 'supersecret'." - }` - ) + if (throwOnError) { + this.rejectErrors( + `http.jwtSecret not found.${ + this.#isProduction ? "" : "Using default 'supersecret'." + }` + ) + } http.jwtSecret = "supersecret" } @@ -110,11 +117,13 @@ export class ConfigManager { process.env.COOKIE_SECRET)! if (!http.cookieSecret) { - this.rejectErrors( - `http.cookieSecret not found.${ - this.#isProduction ? "" : " Using default 'supersecret'." - }` - ) + if (throwOnError) { + this.rejectErrors( + `http.cookieSecret not found.${ + this.#isProduction ? "" : " Using default 'supersecret'." + }` + ) + } http.cookieSecret = "supersecret" } @@ -128,7 +137,10 @@ export class ConfigManager { * @protected */ protected normalizeProjectConfig( - config: Partial + config: Partial, + options?: { + throwOnError?: boolean + } ): ConfigModule["projectConfig"] { const projConfig = config?.projectConfig ?? {} const outputConfig = deepCopy(projConfig) as ConfigModule["projectConfig"] @@ -140,7 +152,9 @@ export class ConfigManager { ) } - outputConfig.http = this.buildHttpConfig(projConfig) + outputConfig.http = this.buildHttpConfig(projConfig, { + throwOnError: options?.throwOnError, + }) let workerMode = outputConfig?.workerMode! @@ -168,13 +182,17 @@ export class ConfigManager { loadConfig({ projectConfig = {}, baseDir, + throwOnError = true, }: { projectConfig: Partial baseDir: string + throwOnError?: boolean }): ConfigModule { this.#baseDir = baseDir - const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig) + const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig, { + throwOnError, + }) this.#config = { projectConfig: normalizedProjectConfig, diff --git a/packages/core/framework/src/config/loader.ts b/packages/core/framework/src/config/loader.ts index 68b48940f8..a546add460 100644 --- a/packages/core/framework/src/config/loader.ts +++ b/packages/core/framework/src/config/loader.ts @@ -25,22 +25,29 @@ container.register( * * @param entryDirectory The directory to find the config file from * @param configFileName The name of the config file to search for in the entry directory + * @param options.throwOnError When false, missing config files and validation errors won't throw. + * Useful for build/compile commands. Defaults to true. */ export async function configLoader( entryDirectory: string, - configFileName: string = "medusa-config" + configFileName: string = "medusa-config", + options?: { + throwOnError?: boolean + } ): Promise { + const { throwOnError = true } = options ?? {} const config = await getConfigFile( entryDirectory, configFileName ) - if (config.error) { + if (config.error && throwOnError) { handleConfigError(config.error) } return configManager.loadConfig({ projectConfig: config.configModule!, baseDir: entryDirectory, + throwOnError, }) } diff --git a/packages/medusa/src/commands/build.ts b/packages/medusa/src/commands/build.ts index e2cd6a29ec..89a99c075d 100644 --- a/packages/medusa/src/commands/build.ts +++ b/packages/medusa/src/commands/build.ts @@ -12,6 +12,7 @@ export default async function build({ }) { const container = await initializeContainer(directory, { skipDbConnection: true, + throwOnError: false, }) const logger = container.resolve(ContainerRegistrationKeys.LOGGER) diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 53d6f2225f..5c65056232 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -134,10 +134,13 @@ export async function initializeContainer( rootDirectory: string, options?: { skipDbConnection?: boolean + throwOnError?: boolean } ): Promise { await featureFlagsLoader(rootDirectory) - const configDir = await configLoader(rootDirectory, "medusa-config") + const configDir = await configLoader(rootDirectory, "medusa-config", { + throwOnError: options?.throwOnError, + }) await featureFlagsLoader(join(__dirname, "..")) const customLogger = configDir.logger ?? defaultLogger