feat(utils): define file config (#13283)

** What
 - Allow auto-loaded Medusa files to export a config object.
 - Currently supports isDisabled to control loading.
 - new instance `FeatureFlag` exported by `@medusajs/framework/utils`
 - `feature-flags` is now a supported folder for medusa projects, modules, providers and plugins. They will be loaded and added to `FeatureFlag`

** Why
 - Enables conditional loading of routes, migrations, jobs, subscribers, workflows, and other files based on feature flags.

```ts
// /src/feature-flags

import { FlagSettings } from "@medusajs/framework/feature-flags"

const CustomFeatureFlag: FlagSettings = {
  key: "custom_feature",
  default_val: false,
  env_key: "FF_MY_CUSTOM_FEATURE",
  description: "Enable xyz",
}

export default CustomFeatureFlag
```

```ts
// /src/modules/my-custom-module/migration/Migration20250822135845.ts

import { FeatureFlag } from "@medusajs/framework/utils"

export class Migration20250822135845 extends Migration {
  override async up(){ }
  override async down(){ }
}

defineFileConfig({
  isDisabled: () => !FeatureFlag.isFeatureEnabled("custom_feature")
})
```
This commit is contained in:
Carlos R. L. Rodrigues
2025-08-26 12:22:30 +00:00
committed by GitHub
parent 4cda412243
commit e413cfefc2
183 changed files with 1089 additions and 605 deletions
@@ -1,7 +1,4 @@
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
export const AUTHENTICATE = false
@@ -13,14 +10,14 @@ export const GET = async (
const featureFlagRouter = req.scope.resolve(
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
) as any
const flags = featureFlagRouter.listFlags()
// Convert array of flags to a simple key-value object
const featureFlags: Record<string, boolean> = {}
flags.forEach(flag => {
flags.forEach((flag) => {
featureFlags[flag.key] = flag.value
})
res.json({ feature_flags: featureFlags })
}
}
@@ -1,10 +1,11 @@
import {
featureFlagRouter,
validateAndTransformBody,
validateAndTransformQuery,
} from "@medusajs/framework"
import multer from "multer"
import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http"
import { FeatureFlag } from "@medusajs/framework/utils"
import multer from "multer"
import IndexEngineFeatureFlag from "../../../feature-flags/index-engine"
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares"
import { createBatchBody } from "../../utils/validators"
import * as QueryConfig from "./query-config"
@@ -33,7 +34,6 @@ import {
CreateProduct,
CreateProductVariant,
} from "./validators"
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
const upload = multer({ storage: multer.memoryStorage() })
@@ -47,7 +47,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
QueryConfig.listProductQueryConfig
),
(req, res, next) => {
if (featureFlagRouter.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
if (FeatureFlag.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
return next()
}
@@ -1,5 +1,4 @@
import { createProductsWorkflow } from "@medusajs/core-flows"
import { featureFlagRouter } from "@medusajs/framework"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
@@ -7,15 +6,19 @@ import {
refetchEntity,
} from "@medusajs/framework/http"
import { AdditionalData, HttpTypes } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, isPresent } from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
import {
ContainerRegistrationKeys,
FeatureFlag,
isPresent,
} from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../feature-flags/index-engine"
import { remapKeysForProduct, remapProductResponse } from "./helpers"
export const GET = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminProductListParams>,
res: MedusaResponse<HttpTypes.AdminProductListResponse>
) => {
if (featureFlagRouter.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
if (FeatureFlag.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
// Use regular list when no filters are provided
// TODO: Tags and categories are not supported by the index engine yet
if (
@@ -1,10 +1,10 @@
import {
MedusaRequest,
MedusaResponse,
MedusaNextFunction
import {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import ViewConfigurationsFeatureFlag from "../../../../../loaders/feature-flags/view-configurations"
import ViewConfigurationsFeatureFlag from "../../../../../feature-flags/view-configurations"
export const ensureViewConfigurationsEnabled = async (
req: MedusaRequest,
@@ -14,14 +14,14 @@ export const ensureViewConfigurationsEnabled = async (
const flagRouter = req.scope.resolve(
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
) as any
if (!flagRouter.isFeatureEnabled(ViewConfigurationsFeatureFlag.key)) {
res.status(404).json({
type: "not_found",
message: "Route not found"
message: "Route not found",
})
return
}
next()
}
}
@@ -1,7 +1,4 @@
import {
featureFlagRouter,
validateAndTransformQuery,
} from "@medusajs/framework"
import { validateAndTransformQuery } from "@medusajs/framework"
import {
applyDefaultFilters,
applyParamsAsFilters,
@@ -10,8 +7,12 @@ import {
maybeApplyLinkFilter,
MiddlewareRoute,
} from "@medusajs/framework/http"
import { isPresent, ProductStatus } from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
import {
FeatureFlag,
isPresent,
ProductStatus,
} from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../feature-flags/index-engine"
import {
filterByValidSalesChannels,
normalizeDataForContext,
@@ -40,7 +41,7 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
isPresent(req.filterableFields.categories)
)
if (
featureFlagRouter.isFeatureEnabled(IndexEngineFeatureFlag.key) &&
FeatureFlag.isFeatureEnabled(IndexEngineFeatureFlag.key) &&
canUseIndex
) {
return next()
@@ -1,13 +1,13 @@
import { featureFlagRouter } from "@medusajs/framework"
import { MedusaResponse } from "@medusajs/framework/http"
import { HttpTypes, QueryContextType } from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
FeatureFlag,
isPresent,
QueryContext,
remoteQueryObjectFromString,
} from "@medusajs/framework/utils"
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
import IndexEngineFeatureFlag from "../../../feature-flags/index-engine"
import { wrapVariantsWithInventoryQuantityForSalesChannel } from "../../utils/middlewares"
import { RequestWithContext, wrapProductsWithTaxPrices } from "./helpers"
@@ -15,7 +15,7 @@ export const GET = async (
req: RequestWithContext<HttpTypes.StoreProductListParams>,
res: MedusaResponse<HttpTypes.StoreProductListResponse>
) => {
if (featureFlagRouter.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
if (FeatureFlag.isFeatureEnabled(IndexEngineFeatureFlag.key)) {
// TODO: These filters are not supported by the index engine yet
if (
isPresent(req.filterableFields.tags) ||
+9 -5
View File
@@ -1,10 +1,10 @@
import loaders from "../loaders"
import express from "express"
import path from "path"
import { existsSync } from "fs"
import { logger } from "@medusajs/framework/logger"
import { ExecArgs } from "@medusajs/framework/types"
import { dynamicImport } from "@medusajs/framework/utils"
import { dynamicImport, isFileSkipped } from "@medusajs/framework/utils"
import express from "express"
import { existsSync } from "fs"
import path from "path"
import loaders from "../loaders"
type Options = {
file: string
@@ -25,6 +25,10 @@ export default async function exec({ file, args }: Options) {
const scriptToExec = (await dynamicImport(path.resolve(filePath))).default
if (isFileSkipped(scriptToExec)) {
throw new Error(`File is disabled.`)
}
if (!scriptToExec || typeof scriptToExec !== "function") {
throw new Error(`File doesn't default export a function to execute.`)
}
@@ -1,11 +1,12 @@
import { glob } from "glob"
import { logger } from "@medusajs/framework/logger"
import {
toUnixSlash,
defineMikroOrmCliConfig,
DmlEntity,
dynamicImport,
defineMikroOrmCliConfig,
isFileSkipped,
toUnixSlash,
} from "@medusajs/framework/utils"
import { glob } from "glob"
import { dirname, join } from "path"
import { MetadataStorage } from "@mikro-orm/core"
@@ -68,6 +69,9 @@ async function getEntitiesForModule(path: string) {
for (const entityPath of entityPaths) {
const entityExports = await dynamicImport(entityPath)
if (isFileSkipped(entityExports)) {
continue
}
const validEntities = Object.values(entityExports).filter(
(potentialEntity) => {
+14 -9
View File
@@ -1,25 +1,26 @@
import { track } from "@medusajs/telemetry"
import cluster from "cluster"
import express from "express"
import http from "http"
import { scheduleJob } from "node-schedule"
import os from "os"
import path from "path"
import http from "http"
import express from "express"
import cluster from "cluster"
import { track } from "@medusajs/telemetry"
import { scheduleJob } from "node-schedule"
import { logger } from "@medusajs/framework/logger"
import {
dynamicImport,
FileSystem,
generateContainerTypes,
gqlSchemaToTypes,
GracefulShutdownServer,
isFileSkipped,
isPresent,
generateContainerTypes,
} from "@medusajs/framework/utils"
import { logger } from "@medusajs/framework/logger"
import loaders from "../loaders"
import { MedusaModule } from "@medusajs/framework/modules-sdk"
import { MedusaContainer } from "@medusajs/framework/types"
import { parse } from "url"
import loaders from "../loaders"
const EVERY_SIXTH_HOUR = "0 */6 * * *"
const CRON_SCHEDULE = EVERY_SIXTH_HOUR
@@ -43,7 +44,11 @@ export async function registerInstrumentation(directory: string) {
const instrumentation = await dynamicImport(
path.join(directory, INSTRUMENTATION_FILE)
)
if (typeof instrumentation.register === "function") {
if (
typeof instrumentation.register === "function" &&
!isFileSkipped(instrumentation)
) {
logger.info("OTEL registered")
instrumentation.register()
} else {
@@ -9,9 +9,6 @@ import express from "express"
import querystring from "querystring"
import supertest from "supertest"
import { config } from "../mocks"
import { ConfigModule, MedusaContainer } from "@medusajs/types"
import { configManager } from "@medusajs/framework/config"
import {
ApiLoader,
container,
@@ -19,6 +16,9 @@ import {
logger,
MedusaRequest,
} from "@medusajs/framework"
import { configManager } from "@medusajs/framework/config"
import { ConfigModule, MedusaContainer } from "@medusajs/types"
import { config } from "../mocks"
function asArray(resolvers) {
return {
@@ -37,12 +37,11 @@ export const createServer = async (rootDir) => {
const moduleResolutions = {}
Object.entries(ModulesDefinition).forEach(([moduleKey, module]) => {
moduleResolutions[moduleKey] = registerMedusaModule(
moduleResolutions[moduleKey] = registerMedusaModule({
moduleKey,
module.defaultModuleDeclaration,
undefined,
module
)[moduleKey]
moduleDeclaration: module.defaultModuleDeclaration,
moduleExports: module as any,
})[moduleKey]
})
configManager.loadConfig({
@@ -1,11 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const AnalyticsFeatureFlag: FlagSettings = {
key: "analytics",
default_val: true,
env_key: "MEDUSA_FF_ANALYTICS",
description:
"Enable Medusa to collect data on usage, errors and performance for the purpose of improving the product",
}
export default AnalyticsFeatureFlag
@@ -1,3 +0,0 @@
import { MedusaV2Flag } from "@medusajs/framework/utils"
export default MedusaV2Flag
@@ -1,10 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const OrderEditingFeatureFlag: FlagSettings = {
key: "order_editing",
default_val: true,
env_key: "MEDUSA_FF_ORDER_EDITING",
description: "[WIP] Enable the order editing feature",
}
export default OrderEditingFeatureFlag
@@ -1,10 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const ProductCategoryFeatureFlag: FlagSettings = {
key: "product_categories",
default_val: false,
env_key: "MEDUSA_FF_PRODUCT_CATEGORIES",
description: "[WIP] Enable the product categories feature",
}
export default ProductCategoryFeatureFlag
@@ -1,10 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const PublishableAPIKeysFeatureFlag: FlagSettings = {
key: "publishable_api_keys",
default_val: true,
env_key: "MEDUSA_FF_PUBLISHABLE_API_KEYS",
description: "[WIP] Enable the publishable API keys feature",
}
export default PublishableAPIKeysFeatureFlag
@@ -1,10 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const SalesChannelFeatureFlag: FlagSettings = {
key: "sales_channels",
default_val: true,
env_key: "MEDUSA_FF_SALES_CHANNELS",
description: "[WIP] Enable the sales channels feature",
}
export default SalesChannelFeatureFlag
@@ -1,10 +0,0 @@
import { FlagSettings } from "@medusajs/framework/feature-flags"
const TaxInclusivePricingFeatureFlag: FlagSettings = {
key: "tax_inclusive_pricing",
default_val: false,
env_key: "MEDUSA_FF_TAX_INCLUSIVE_PRICING",
description: "[WIP] Enable tax inclusive pricing",
}
export default TaxInclusivePricingFeatureFlag
@@ -1,3 +0,0 @@
import { WorkflowsFeatureFlag } from "@medusajs/framework/utils"
export default WorkflowsFeatureFlag
+1 -1
View File
@@ -126,7 +126,7 @@ export async function initializeContainer(
rootDirectory: string
): Promise<MedusaContainer> {
await configLoader(rootDirectory, "medusa-config")
await featureFlagsLoader(join(__dirname, "feature-flags"))
await featureFlagsLoader(join(__dirname, ".."))
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),