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:
committed by
GitHub
parent
4cda412243
commit
e413cfefc2
@@ -1,4 +1,4 @@
|
||||
import { InternalModuleDeclaration, ModuleDefinition } from "@medusajs/types"
|
||||
import { ModuleDefinition } from "@medusajs/types"
|
||||
import { ModulesDefinition } from "../../definitions"
|
||||
import { MODULE_SCOPE } from "../../types"
|
||||
import { registerMedusaModule } from "../register-modules"
|
||||
@@ -35,7 +35,9 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -50,10 +52,13 @@ describe("module definitions loader", () => {
|
||||
})
|
||||
|
||||
it("Resolves a custom module without pre-defined definition", () => {
|
||||
const res = registerMedusaModule("customModulesABC", {
|
||||
resolve: testServiceResolved,
|
||||
options: {
|
||||
test: 123,
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: "customModulesABC",
|
||||
moduleDeclaration: {
|
||||
resolve: testServiceResolved,
|
||||
options: {
|
||||
test: 123,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -80,7 +85,10 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, false)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: false,
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -98,7 +106,10 @@ describe("module definitions loader", () => {
|
||||
})
|
||||
|
||||
try {
|
||||
registerMedusaModule(defaultDefinition.key, false)
|
||||
registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: false,
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Module: ${defaultDefinition.label} is required`
|
||||
@@ -118,7 +129,9 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: definition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -138,10 +151,10 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(
|
||||
defaultDefinition.key,
|
||||
defaultDefinition.defaultPackage
|
||||
)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: defaultDefinition.defaultPackage,
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -162,10 +175,13 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
} as InternalModuleDeclaration)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resolve: defaultDefinition.defaultPackage as string,
|
||||
},
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -186,9 +202,12 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
options: { test: 123 },
|
||||
} as any)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: {
|
||||
options: { test: 123 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -209,11 +228,14 @@ describe("module definitions loader", () => {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
options: { test: 123 },
|
||||
scope: "internal",
|
||||
} as any)
|
||||
const res = registerMedusaModule({
|
||||
moduleKey: defaultDefinition.key,
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resolve: defaultDefinition.defaultPackage as string,
|
||||
options: { test: 123 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MedusaContainer, ModuleProvider } from "@medusajs/types"
|
||||
import {
|
||||
dynamicImport,
|
||||
isFileSkipped,
|
||||
isString,
|
||||
lowerCaseFirst,
|
||||
normalizeImportPathWithSource,
|
||||
@@ -53,6 +54,10 @@ export async function loadModuleProvider(
|
||||
)
|
||||
}
|
||||
|
||||
if (isFileSkipped(loadedProvider)) {
|
||||
return
|
||||
}
|
||||
|
||||
loadedProvider = (loadedProvider as any).default ?? loadedProvider
|
||||
|
||||
if (!loadedProvider?.services?.length) {
|
||||
|
||||
@@ -14,15 +14,22 @@ import {
|
||||
import { ModulesDefinition } from "../definitions"
|
||||
import { MODULE_SCOPE } from "../types"
|
||||
|
||||
export const registerMedusaModule = (
|
||||
moduleKey: string,
|
||||
export const registerMedusaModule = ({
|
||||
moduleKey,
|
||||
moduleDeclaration,
|
||||
moduleExports,
|
||||
definition,
|
||||
cwd,
|
||||
}: {
|
||||
moduleKey: string
|
||||
moduleDeclaration?:
|
||||
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
|
||||
| string
|
||||
| false,
|
||||
moduleExports?: ModuleExports,
|
||||
| false
|
||||
moduleExports?: ModuleExports
|
||||
definition?: ModuleDefinition
|
||||
): Record<string, ModuleResolution> => {
|
||||
cwd?: string
|
||||
}): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
const modDefinition = definition ?? ModulesDefinition[moduleKey]
|
||||
@@ -46,7 +53,8 @@ export const registerMedusaModule = (
|
||||
if (modDefinition === undefined) {
|
||||
moduleResolutions[moduleKey] = getCustomModuleResolution(
|
||||
moduleKey,
|
||||
moduleDeclaration as InternalModuleDeclaration
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
cwd
|
||||
)
|
||||
return moduleResolutions
|
||||
}
|
||||
@@ -54,7 +62,8 @@ export const registerMedusaModule = (
|
||||
moduleResolutions[moduleKey] = getInternalModuleResolution(
|
||||
modDefinition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
moduleExports,
|
||||
cwd
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
@@ -62,13 +71,15 @@ export const registerMedusaModule = (
|
||||
|
||||
function getCustomModuleResolution(
|
||||
key: string,
|
||||
moduleConfig: InternalModuleDeclaration | string
|
||||
moduleConfig: InternalModuleDeclaration | string,
|
||||
cwd: string = process.cwd()
|
||||
): ModuleResolution {
|
||||
const originalPath = normalizeImportPathWithSource(
|
||||
(isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string
|
||||
(isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string,
|
||||
cwd
|
||||
)
|
||||
const resolutionPath = require.resolve(originalPath, {
|
||||
paths: [process.cwd()],
|
||||
paths: [cwd],
|
||||
})
|
||||
|
||||
const conf = isObject(moduleConfig)
|
||||
@@ -100,14 +111,16 @@ function getCustomModuleResolution(
|
||||
export const registerMedusaLinkModule = (
|
||||
definition: ModuleDefinition,
|
||||
moduleDeclaration: Partial<InternalModuleDeclaration>,
|
||||
moduleExports?: ModuleExports
|
||||
moduleExports?: ModuleExports,
|
||||
cwd: string = process.cwd()
|
||||
): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
moduleResolutions[definition.key] = getInternalModuleResolution(
|
||||
definition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
moduleExports,
|
||||
cwd
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
@@ -116,7 +129,8 @@ export const registerMedusaLinkModule = (
|
||||
function getInternalModuleResolution(
|
||||
definition: ModuleDefinition,
|
||||
moduleConfig: InternalModuleDeclaration | string | false,
|
||||
moduleExports?: ModuleExports
|
||||
moduleExports?: ModuleExports,
|
||||
cwd: string = process.cwd()
|
||||
): ModuleResolution {
|
||||
if (typeof moduleConfig === "boolean") {
|
||||
if (!moduleConfig && definition.isRequired) {
|
||||
@@ -140,10 +154,11 @@ function getInternalModuleResolution(
|
||||
const isStr = isString(moduleConfig)
|
||||
if (isStr || (isObj && moduleConfig.resolve)) {
|
||||
const originalPath = normalizeImportPathWithSource(
|
||||
(isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string
|
||||
(isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string,
|
||||
cwd
|
||||
)
|
||||
resolutionPath = require.resolve(originalPath, {
|
||||
paths: [process.cwd()],
|
||||
paths: [cwd],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ModuleProviderService as ModuleServiceWithProviderProvider1 } from "../
|
||||
import { ModuleProvider2Service as ModuleServiceWithProviderProvider2 } from "../__fixtures__/module-with-providers/provider-2"
|
||||
import { loadInternalModule, loadResources } from "../load-internal"
|
||||
|
||||
const container = createMedusaContainer()
|
||||
describe("load internal", () => {
|
||||
describe("loadResources", () => {
|
||||
describe("when loading the module resources from a path", () => {
|
||||
@@ -43,6 +44,7 @@ describe("load internal", () => {
|
||||
).toBeUndefined()
|
||||
|
||||
const resources = await loadResources({
|
||||
container,
|
||||
moduleResolution,
|
||||
discoveryPath: moduleResolution.resolutionPath as string,
|
||||
})
|
||||
@@ -125,6 +127,7 @@ describe("load internal", () => {
|
||||
).toBeUndefined()
|
||||
|
||||
const resources = await loadResources({
|
||||
container,
|
||||
moduleResolution,
|
||||
discoveryPath: moduleResolution.resolutionPath as string,
|
||||
})
|
||||
@@ -207,6 +210,7 @@ describe("load internal", () => {
|
||||
).toBeUndefined()
|
||||
|
||||
const resources = await loadResources({
|
||||
container,
|
||||
moduleResolution,
|
||||
discoveryPath: moduleResolution.resolutionPath as string,
|
||||
})
|
||||
@@ -288,6 +292,7 @@ describe("load internal", () => {
|
||||
).toBeDefined()
|
||||
|
||||
const resources = await loadResources({
|
||||
container,
|
||||
moduleResolution,
|
||||
discoveryPath: moduleResolution.resolutionPath as string,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ConfigModule,
|
||||
Constructor,
|
||||
IModuleService,
|
||||
InternalModuleDeclaration,
|
||||
@@ -16,14 +17,18 @@ import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
defineJoinerConfig,
|
||||
discoverFeatureFlagsFromDir,
|
||||
DmlEntity,
|
||||
dynamicImport,
|
||||
FeatureFlag,
|
||||
getProviderRegistrationKey,
|
||||
isFileSkipped,
|
||||
isString,
|
||||
MedusaModuleProviderType,
|
||||
MedusaModuleType,
|
||||
Modules,
|
||||
ModulesSdkUtils,
|
||||
registerFeatureFlag,
|
||||
stringifyCircular,
|
||||
toMikroOrmEntities,
|
||||
} from "@medusajs/utils"
|
||||
@@ -193,6 +198,7 @@ export async function loadInternalModule(args: {
|
||||
|
||||
if (loadedModule.discoveryPath) {
|
||||
moduleResources = await loadResources({
|
||||
container,
|
||||
moduleResolution: resolution,
|
||||
discoveryPath: loadedModule.discoveryPath,
|
||||
logger,
|
||||
@@ -379,6 +385,7 @@ export async function loadInternalModule(args: {
|
||||
}
|
||||
|
||||
export async function loadModuleMigrations(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<{
|
||||
@@ -449,6 +456,7 @@ export async function loadModuleMigrations(
|
||||
|
||||
if (!runMigrationsCustom || !revertMigrationCustom) {
|
||||
const moduleResources = await loadResources({
|
||||
container,
|
||||
moduleResolution: resolution,
|
||||
discoveryPath: loadedModule.discoveryPath,
|
||||
loadedModuleLoaders: loadedModule?.loaders,
|
||||
@@ -536,17 +544,21 @@ async function importAllFromDir(path: string) {
|
||||
|
||||
return (
|
||||
await Promise.all(filesToLoad.map((filePath) => dynamicImport(filePath)))
|
||||
).flatMap((value) => {
|
||||
return Object.values(value)
|
||||
})
|
||||
)
|
||||
.filter((value) => !isFileSkipped(value))
|
||||
.flatMap((value) => {
|
||||
return Object.values(value)
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadResources({
|
||||
container,
|
||||
moduleResolution,
|
||||
discoveryPath,
|
||||
logger,
|
||||
loadedModuleLoaders,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
moduleResolution: ModuleResolution
|
||||
discoveryPath: string
|
||||
logger?: Logger
|
||||
@@ -566,8 +578,31 @@ export async function loadResources({
|
||||
return []
|
||||
}
|
||||
|
||||
const flagDir = resolve(normalizedPath)
|
||||
const discovered = await discoverFeatureFlagsFromDir(flagDir, 1)
|
||||
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE,
|
||||
{
|
||||
allowUnregistered: true,
|
||||
}
|
||||
) as ConfigModule
|
||||
|
||||
for (const def of discovered) {
|
||||
registerFeatureFlag({
|
||||
flag: def,
|
||||
projectConfigFlags: configModule?.featureFlags ?? {},
|
||||
router: FeatureFlag,
|
||||
logger,
|
||||
})
|
||||
}
|
||||
|
||||
const [moduleService, services, models, repositories] = await Promise.all([
|
||||
dynamicImport(modulePath).then((moduleExports) => {
|
||||
if (isFileSkipped(moduleExports)) {
|
||||
return
|
||||
}
|
||||
|
||||
const mod = moduleExports.default ?? moduleExports
|
||||
return mod.service
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user