feat(medusa): Prevent build command from throwing on missing config (#14540)

**What**
Prevent the build command from failing on mising config
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2026-01-15 09:00:15 +00:00
1 parent 250b6fdf22
commit 8890f28470
6 files changed
+73 -7

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/framework": patch
---
feat(medusa): Prevent build command from throwing on missing config
@@ -38,4 +38,35 @@ describe("configLoader", () => {
expect(configModule.projectConfig.databaseName).toBe("foo") expect(configModule.projectConfig.databaseName).toBe("foo")
expect(configModule.projectConfig.workerMode).toBe("worker") 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")
})
}) })
+22 -4
View File
@@ -70,8 +70,12 @@ export class ConfigManager {
* @protected * @protected
*/ */
protected buildHttpConfig( protected buildHttpConfig(
projectConfig: Partial<ConfigModule["projectConfig"]> projectConfig: Partial<ConfigModule["projectConfig"]>,
options?: {
throwOnError?: boolean
}
): ConfigModule["projectConfig"]["http"] { ): ConfigModule["projectConfig"]["http"] {
const { throwOnError = true } = options ?? {}
const http = (projectConfig.http ?? const http = (projectConfig.http ??
{}) as ConfigModule["projectConfig"]["http"] {}) as ConfigModule["projectConfig"]["http"]
@@ -87,6 +91,7 @@ export class ConfigManager {
http.jwtPublicKey = http?.jwtPublicKey ?? process.env.JWT_PUBLIC_KEY http.jwtPublicKey = http?.jwtPublicKey ?? process.env.JWT_PUBLIC_KEY
if ( if (
throwOnError &&
http?.jwtPublicKey && http?.jwtPublicKey &&
((http.jwtVerifyOptions && !http.jwtVerifyOptions.algorithms?.length) || ((http.jwtVerifyOptions && !http.jwtVerifyOptions.algorithms?.length) ||
(http.jwtOptions && !http.jwtOptions.algorithm)) (http.jwtOptions && !http.jwtOptions.algorithm))
@@ -97,11 +102,13 @@ export class ConfigManager {
} }
if (!http.jwtSecret) { if (!http.jwtSecret) {
if (throwOnError) {
this.rejectErrors( this.rejectErrors(
`http.jwtSecret not found.${ `http.jwtSecret not found.${
this.#isProduction ? "" : "Using default 'supersecret'." this.#isProduction ? "" : "Using default 'supersecret'."
}` }`
) )
}
http.jwtSecret = "supersecret" http.jwtSecret = "supersecret"
} }
@@ -110,11 +117,13 @@ export class ConfigManager {
process.env.COOKIE_SECRET)! process.env.COOKIE_SECRET)!
if (!http.cookieSecret) { if (!http.cookieSecret) {
if (throwOnError) {
this.rejectErrors( this.rejectErrors(
`http.cookieSecret not found.${ `http.cookieSecret not found.${
this.#isProduction ? "" : " Using default 'supersecret'." this.#isProduction ? "" : " Using default 'supersecret'."
}` }`
) )
}
http.cookieSecret = "supersecret" http.cookieSecret = "supersecret"
} }
@@ -128,7 +137,10 @@ export class ConfigManager {
* @protected * @protected
*/ */
protected normalizeProjectConfig( protected normalizeProjectConfig(
config: Partial<ConfigModule> config: Partial<ConfigModule>,
options?: {
throwOnError?: boolean
}
): ConfigModule["projectConfig"] { ): ConfigModule["projectConfig"] {
const projConfig = config?.projectConfig ?? {} const projConfig = config?.projectConfig ?? {}
const outputConfig = deepCopy(projConfig) as ConfigModule["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! let workerMode = outputConfig?.workerMode!
@@ -168,13 +182,17 @@ export class ConfigManager {
loadConfig({ loadConfig({
projectConfig = {}, projectConfig = {},
baseDir, baseDir,
throwOnError = true,
}: { }: {
projectConfig: Partial<ConfigModule> projectConfig: Partial<ConfigModule>
baseDir: string baseDir: string
throwOnError?: boolean
}): ConfigModule { }): ConfigModule {
this.#baseDir = baseDir this.#baseDir = baseDir
const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig) const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig, {
throwOnError,
})
this.#config = { this.#config = {
projectConfig: normalizedProjectConfig, projectConfig: normalizedProjectConfig,
+9 -2
View File
@@ -25,22 +25,29 @@ container.register(
* *
* @param entryDirectory The directory to find the config file from * @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 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( export async function configLoader(
entryDirectory: string, entryDirectory: string,
configFileName: string = "medusa-config" configFileName: string = "medusa-config",
options?: {
throwOnError?: boolean
}
): Promise<ConfigModule> { ): Promise<ConfigModule> {
const { throwOnError = true } = options ?? {}
const config = await getConfigFile<ConfigModule>( const config = await getConfigFile<ConfigModule>(
entryDirectory, entryDirectory,
configFileName configFileName
) )
if (config.error) { if (config.error && throwOnError) {
handleConfigError(config.error) handleConfigError(config.error)
} }
return configManager.loadConfig({ return configManager.loadConfig({
projectConfig: config.configModule!, projectConfig: config.configModule!,
baseDir: entryDirectory, baseDir: entryDirectory,
throwOnError,
}) })
} }
+1
View File
@@ -12,6 +12,7 @@ export default async function build({
}) { }) {
const container = await initializeContainer(directory, { const container = await initializeContainer(directory, {
skipDbConnection: true, skipDbConnection: true,
throwOnError: false,
}) })
const logger = container.resolve(ContainerRegistrationKeys.LOGGER) const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
+4 -1
View File
@@ -134,10 +134,13 @@ export async function initializeContainer(
rootDirectory: string, rootDirectory: string,
options?: { options?: {
skipDbConnection?: boolean skipDbConnection?: boolean
throwOnError?: boolean
} }
): Promise<MedusaContainer> { ): Promise<MedusaContainer> {
await featureFlagsLoader(rootDirectory) await featureFlagsLoader(rootDirectory)
const configDir = await configLoader(rootDirectory, "medusa-config") const configDir = await configLoader(rootDirectory, "medusa-config", {
throwOnError: options?.throwOnError,
})
await featureFlagsLoader(join(__dirname, "..")) await featureFlagsLoader(join(__dirname, ".."))
const customLogger = configDir.logger ?? defaultLogger const customLogger = configDir.logger ?? defaultLogger