feat: Add support for generating a cloud-ready config out of the box (#11850)

This commit is contained in:
Stevche Radevski
2025-03-21 09:21:38 +01:00
committed by GitHub
parent c40fb01d92
commit c4f75ecbb2
3 changed files with 345 additions and 86 deletions

View File

@@ -958,4 +958,188 @@ describe("defineConfig", function () {
}
`)
})
it("should include cloud-based modules when in cloud execution context", function () {
const originalEnv = { ...process.env }
process.env.EXECUTION_CONTEXT = "medusa-cloud"
process.env.REDIS_URL = "redis://localhost:6379"
process.env.S3_FILE_URL = "https://s3.amazonaws.com/medusa-cloud-test"
process.env.S3_PREFIX = "test"
process.env.S3_REGION = "us-east-1"
process.env.S3_BUCKET = "medusa-cloud-test"
process.env.S3_ENDPOINT = "https://s3.amazonaws.com"
const res = defineConfig({})
process.env = { ...originalEnv }
expect(res).toMatchInlineSnapshot(`
{
"admin": {
"backendUrl": "/",
"path": "/app",
},
"featureFlags": {},
"modules": {
"api_key": {
"resolve": "@medusajs/medusa/api-key",
},
"auth": {
"options": {
"providers": [
{
"id": "emailpass",
"resolve": "@medusajs/medusa/auth-emailpass",
},
],
},
"resolve": "@medusajs/medusa/auth",
},
"cache": {
"options": {
"redisUrl": "redis://localhost:6379",
},
"resolve": "@medusajs/medusa/cache-redis",
},
"cart": {
"resolve": "@medusajs/medusa/cart",
},
"currency": {
"resolve": "@medusajs/medusa/currency",
},
"customer": {
"resolve": "@medusajs/medusa/customer",
},
"event_bus": {
"options": {
"redisUrl": "redis://localhost:6379",
},
"resolve": "@medusajs/medusa/event-bus-redis",
},
"file": {
"options": {
"providers": [
{
"id": "s3",
"options": {
"authentication_method": "s3-iam-role",
"bucket": "medusa-cloud-test",
"endpoint": "https://s3.amazonaws.com",
"file_url": "https://s3.amazonaws.com/medusa-cloud-test",
"prefix": "test",
"region": "us-east-1",
},
"resolve": "@medusajs/medusa/file-s3",
},
],
},
"resolve": "@medusajs/medusa/file",
},
"fulfillment": {
"options": {
"providers": [
{
"id": "manual",
"resolve": "@medusajs/medusa/fulfillment-manual",
},
],
},
"resolve": "@medusajs/medusa/fulfillment",
},
"inventory": {
"resolve": "@medusajs/medusa/inventory",
},
"locking": {
"options": {
"redisUrl": "redis://localhost:6379",
},
"resolve": "@medusajs/medusa/locking-redis",
},
"notification": {
"options": {
"providers": [
{
"id": "local",
"options": {
"channels": [
"feed",
],
"name": "Local Notification Provider",
},
"resolve": "@medusajs/medusa/notification-local",
},
],
},
"resolve": "@medusajs/medusa/notification",
},
"order": {
"resolve": "@medusajs/medusa/order",
},
"payment": {
"resolve": "@medusajs/medusa/payment",
},
"pricing": {
"resolve": "@medusajs/medusa/pricing",
},
"product": {
"resolve": "@medusajs/medusa/product",
},
"promotion": {
"resolve": "@medusajs/medusa/promotion",
},
"region": {
"resolve": "@medusajs/medusa/region",
},
"sales_channel": {
"resolve": "@medusajs/medusa/sales-channel",
},
"stock_location": {
"resolve": "@medusajs/medusa/stock-location",
},
"store": {
"resolve": "@medusajs/medusa/store",
},
"tax": {
"resolve": "@medusajs/medusa/tax",
},
"user": {
"options": {
"jwt_secret": "supersecret",
},
"resolve": "@medusajs/medusa/user",
},
"workflows": {
"options": {
"redis": {
"url": "redis://localhost:6379",
},
},
"resolve": "@medusajs/medusa/workflow-engine-redis",
},
},
"plugins": [],
"projectConfig": {
"databaseUrl": "postgres://localhost/medusa-starter-default",
"http": {
"adminCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
"authCors": "http://localhost:7000,http://localhost:7001,http://localhost:5173",
"cookieSecret": "supersecret",
"jwtSecret": "supersecret",
"restrictedFields": {
"store": [
${DEFAULT_STORE_RESTRICTED_FIELDS.map((v) => `"${v}"`).join(
",\n "
)},
],
},
"storeCors": "http://localhost:8000",
},
"redisOptions": {
"retryStrategy": [Function],
},
"redisUrl": "redis://localhost:6379",
},
}
`)
})
})

View File

@@ -8,12 +8,14 @@ import {
MODULE_PACKAGE_NAMES,
Modules,
REVERSED_MODULE_PACKAGE_NAMES,
TEMPORARY_REDIS_MODULE_PACKAGE_NAMES,
} from "../modules-sdk"
import { isObject } from "./is-object"
import { isString } from "./is-string"
import { normalizeImportPathWithSource } from "./normalize-import-path-with-source"
import { resolveExports } from "./resolve-exports"
const MEDUSA_CLOUD_EXECUTION_CONTEXT = "medusa-cloud"
const DEFAULT_SECRET = "supersecret"
const DEFAULT_ADMIN_URL = "/"
const DEFAULT_STORE_CORS = "http://localhost:8000"
@@ -39,71 +41,19 @@ export const DEFAULT_STORE_RESTRICTED_FIELDS = [
* to override configuration as needed.
*/
export function defineConfig(config: InputConfig = {}): ConfigModule {
const { http, redisOptions, ...restOfProjectConfig } =
config.projectConfig || {}
/**
* The defaults to use for the project config. They are shallow merged
* with the user defined config. However,
*/
const projectConfig: ConfigModule["projectConfig"] = {
databaseUrl: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
http: {
storeCors: process.env.STORE_CORS || DEFAULT_STORE_CORS,
adminCors: process.env.ADMIN_CORS || DEFAULT_ADMIN_CORS,
authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS,
jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET,
cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET,
restrictedFields: {
store: DEFAULT_STORE_RESTRICTED_FIELDS,
},
...http,
},
redisOptions: {
retryStrategy(retries) {
/**
* Exponentially increase delay with every retry
* attempt. Max to 4s
*/
const delay = Math.min(Math.pow(2, retries) * 50, 4000)
/**
* Add a random jitter to not choke the server when multiple
* clients are retrying at the same time
*/
const jitter = Math.floor(Math.random() * 200)
return delay + jitter
},
...redisOptions,
},
...restOfProjectConfig,
const options = {
isCloud: process.env.EXECUTION_CONTEXT === MEDUSA_CLOUD_EXECUTION_CONTEXT,
}
/**
* The defaults to use for the admin config. They are shallow merged
* with the user defined config
*/
const admin: ConfigModule["admin"] = {
backendUrl: process.env.MEDUSA_BACKEND_URL || DEFAULT_ADMIN_URL,
path: "/app",
...config.admin,
}
/**
* The defaults to use for the feature flags config. They are shallow merged
* with the user defined config
*/
const featureFlags: ConfigModule["featureFlags"] = {
...config.featureFlags,
}
const modules = resolveModules(config.modules)
const projectConfig = normalizeProjectConfig(config.projectConfig, options)
const adminConfig = normalizeAdminConfig(config.admin)
const modules = resolveModules(config.modules, options)
return {
projectConfig,
featureFlags,
featureFlags: (config.featureFlags ?? {}) as ConfigModule["featureFlags"],
plugins: config.plugins || [],
admin,
admin: adminConfig,
modules: modules,
}
}
@@ -180,18 +130,10 @@ export function transformModules(
* @param configModules
*/
function resolveModules(
configModules: InputConfig["modules"]
configModules: InputConfig["modules"],
{ isCloud }: { isCloud: boolean }
): Exclude<ConfigModule["modules"], undefined> {
/**
* The default set of modules to always use. The end user can swap
* the modules by providing an alternate implementation via their
* config. But they can never remove a module from this list.
*/
const modules: InputConfig["modules"] = [
{ resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.LOCKING] },
const sharedModules = [
{ resolve: MODULE_PACKAGE_NAMES[Modules.STOCK_LOCATION] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.INVENTORY] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.PRODUCT] },
@@ -226,17 +168,6 @@ function resolveModules(
jwt_secret: process.env.JWT_SECRET ?? DEFAULT_SECRET,
},
},
{
resolve: MODULE_PACKAGE_NAMES[Modules.FILE],
options: {
providers: [
{
resolve: "@medusajs/medusa/file-local",
id: "local",
},
],
},
},
{
resolve: MODULE_PACKAGE_NAMES[Modules.FULFILLMENT],
options: {
@@ -265,6 +196,77 @@ function resolveModules(
},
]
const defaultModules = [
...sharedModules,
{ resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] },
{ resolve: MODULE_PACKAGE_NAMES[Modules.LOCKING] },
{
resolve: MODULE_PACKAGE_NAMES[Modules.FILE],
options: {
providers: [
{
resolve: "@medusajs/medusa/file-local",
id: "local",
},
],
},
},
]
const cloudModules = [
...sharedModules,
{
resolve: TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE],
options: {
redis: { url: process.env.REDIS_URL },
},
},
{
resolve: TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.CACHE],
options: { redisUrl: process.env.REDIS_URL },
},
{
resolve: TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.EVENT_BUS],
options: { redisUrl: process.env.REDIS_URL },
},
{
resolve: TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.LOCKING],
options: { redisUrl: process.env.REDIS_URL },
},
{
resolve: MODULE_PACKAGE_NAMES[Modules.FILE],
options: {
providers: [
{
id: "s3",
resolve: "@medusajs/medusa/file-s3",
options: {
authentication_method: "s3-iam-role",
file_url: process.env.S3_FILE_URL,
prefix: process.env.S3_PREFIX,
region: process.env.S3_REGION,
bucket: process.env.S3_BUCKET,
endpoint: process.env.S3_ENDPOINT,
},
},
],
},
},
]
/**
* The default set of modules to always use. The end user can swap
* the modules by providing an alternate implementation via their
* config. But they can never remove a module from this list.
*/
const modules: InputConfig["modules"] = isCloud
? cloudModules
: defaultModules
/**
* Backward compatibility for the old way of defining modules (object vs array)
*/
@@ -293,3 +295,62 @@ function resolveModules(
return transformModules(modules)
}
function normalizeProjectConfig(
projectConfig: InputConfig["projectConfig"],
{ isCloud }: { isCloud: boolean }
): ConfigModule["projectConfig"] {
const { http, redisOptions, ...restOfProjectConfig } = projectConfig || {}
/**
* The defaults to use for the project config. They are shallow merged
* with the user defined config.
*/
return {
...(isCloud ? { redisUrl: process.env.REDIS_URL } : {}),
databaseUrl: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
http: {
storeCors: process.env.STORE_CORS || DEFAULT_STORE_CORS,
adminCors: process.env.ADMIN_CORS || DEFAULT_ADMIN_CORS,
authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS,
jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET,
cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET,
restrictedFields: {
store: DEFAULT_STORE_RESTRICTED_FIELDS,
},
...http,
},
redisOptions: {
retryStrategy(retries) {
/**
* Exponentially increase delay with every retry
* attempt. Max to 4s
*/
const delay = Math.min(Math.pow(2, retries) * 50, 4000)
/**
* Add a random jitter to not choke the server when multiple
* clients are retrying at the same time
*/
const jitter = Math.floor(Math.random() * 200)
return delay + jitter
},
...redisOptions,
},
...restOfProjectConfig,
}
}
function normalizeAdminConfig(
adminConfig: InputConfig["admin"]
): ConfigModule["admin"] {
/**
* The defaults to use for the admin config. They are shallow merged
* with the user defined config
*/
return {
backendUrl: process.env.MEDUSA_BACKEND_URL || DEFAULT_ADMIN_URL,
path: "/app",
...adminConfig,
}
}

View File

@@ -64,11 +64,25 @@ export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(
}, {})
// TODO: temporary fix until the event bus, cache and workflow engine are migrated to use providers and therefore only a single resolution will be good
REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/event-bus-redis"] =
Modules.EVENT_BUS
REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/cache-redis"] = Modules.CACHE
REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/workflow-engine-redis"] =
Modules.WORKFLOW_ENGINE
export const TEMPORARY_REDIS_MODULE_PACKAGE_NAMES = {
[Modules.EVENT_BUS]: "@medusajs/medusa/event-bus-redis",
[Modules.CACHE]: "@medusajs/medusa/cache-redis",
[Modules.WORKFLOW_ENGINE]: "@medusajs/medusa/workflow-engine-redis",
[Modules.LOCKING]: "@medusajs/medusa/locking-redis",
}
REVERSED_MODULE_PACKAGE_NAMES[
TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.EVENT_BUS]
] = Modules.EVENT_BUS
REVERSED_MODULE_PACKAGE_NAMES[
TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.CACHE]
] = Modules.CACHE
REVERSED_MODULE_PACKAGE_NAMES[
TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE]
] = Modules.WORKFLOW_ENGINE
REVERSED_MODULE_PACKAGE_NAMES[
TEMPORARY_REDIS_MODULE_PACKAGE_NAMES[Modules.LOCKING]
] = Modules.LOCKING
/**
* Making modules be referenced as a type as well.