Repository: artusjs/core
Pull request #43 (2022-04-23 14:41:36)

feat: support load framework & plugin by env
Reference:
* https://github.com/artusjs/core/issues/42
* https://github.com/artusjs/core/issues/36


Patch:
diff --git a/src/application.ts b/src/application.ts
index 5e64940..fb60797 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -18,7 +18,7 @@ export class ArtusApplication implements Application {
   constructor(opts?: ApplicationInitOptions) {
     this.container = new Container(opts?.containerName ?? ArtusInjectEnum.DefaultContainerName);
     this.lifecycleManager = new LifecycleManager(this, this.container);
-    this.loaderFactory = LoaderFactory.create(this.container);
+    this.loaderFactory = LoaderFactory.create(this.container, opts?.envUnits);
 
     process.on('SIGINT', () => this.close());
     process.on('SIGTERM', () => this.close());
@@ -74,6 +74,10 @@ export class ArtusApplication implements Application {
     // Load user manifest
     this.manifest = manifest;
 
+    // load env units
+    await this.loaderFactory.loadEnvUnits(manifest);
+
+    // load other files
     await this.loaderFactory.loadManifest(manifest);
 
     await this.lifecycleManager.emitHook('didLoad');
diff --git a/src/configuration/decorator.ts b/src/configuration/decorator.ts
new file mode 100644
index 0000000..ae87407
--- /dev/null
+++ b/src/configuration/decorator.ts
@@ -0,0 +1,10 @@
+import { HOOK_CONFIG_HANDLE } from "../constraints";
+
+export function DefineConfigHandle(handleName?: string): PropertyDecorator {
+  return (target: any, propertyKey: string | symbol) => {
+    if (typeof propertyKey === 'symbol') {
+      throw new Error(`hookName is not support symbol [${propertyKey.description}]`);
+    }
+    Reflect.defineMetadata(`${HOOK_CONFIG_HANDLE}${handleName}`, propertyKey, target.constructor);
+  };
+};
\ No newline at end of file
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index 01a9303..594cb59 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -1,15 +1,19 @@
 import { Injectable } from '@artus/injection';
 import { ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV } from '../constraints';
 import { mergeConfig } from '../loader/utils/merge';
+import { BasePlugin, PluginFactory } from '../plugin';
+import { PluginConfigItem } from '../plugin/types';
+import { DefineConfigHandle } from './decorator';
 
-type ConfigObject = Record<string, any>;
-type PackageObject = ConfigObject;
-type Framework = { path: string };
+export type ConfigObject = Record<string, any>;
+export type PackageObject = ConfigObject;
+export type EnvUnit = { path: string, choose?: string, ignore?: EnvUnit[] };
+export type FrameworkOptions = { env: string, unitName: string };
 
 @Injectable()
 export default class ConfigurationHandler {
   private configStore: Map<string, ConfigObject> = new Map();
-  private frameworks: Map<string, Framework[]> = new Map();
+  private frameworks: Map<string, EnvUnit[]> = new Map();
   private packages: Map<string, PackageObject[]> = new Map();
 
   getMergedConfig(env?: string): ConfigObject {
@@ -24,14 +28,87 @@ export default class ConfigurationHandler {
     this.configStore.set(env, mergeConfig(storedConfig, config));
   }
 
-  getFrameworks(): Map<string, Framework[]> {
-    return this.frameworks;
+  @DefineConfigHandle('plugin-config')
+  async getPluginConfig(env?: string): Promise<Map<string, EnvUnit>> {
+    const currentEnv = env ?? process.env[ARTUS_SERVER_ENV] ?? ARTUS_DEFAULT_CONFIG_ENV.DEV;
+    const mergedConfig = this.getMergedConfig(currentEnv)?.plugin;
+    const pluginSortedList = await PluginFactory.createFromConfig(mergedConfig || {});
+
+    // get all plugins
+    const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
+    for (const rawEnv of this.configStore.keys()) {
+      const config: any = this.configStore.get(rawEnv) ?? {};
+      const pluginConfig: Record<string, PluginConfigItem> = config.plugin;
+      if (!pluginConfig) {
+        continue;
+      }
+
+      for (const [name, config] of Object.entries(pluginConfig)) {
+        if (!BasePlugin.checkGetPluginConfig(name, config as any, false)) {
+          continue;
+        }
+
+        const items = pluginsConfig.get(name);
+        if (Array.isArray(items)) {
+          items.push(config);
+          continue;
+        }
+        pluginsConfig.set(name, [config]);
+      }
+    }
+
+    const allPlugins: BasePlugin[] = await PluginFactory.createFromConfigList(pluginsConfig);
+
+    const pluginMap = new Map<string, EnvUnit>();
+    for (const plugin of pluginSortedList) {
+      const unit: EnvUnit = { path: plugin.importPath, choose: currentEnv, ignore: [] };
+
+      // ignore disabled
+      if (!plugin.enable) {
+        unit.ignore?.push(unit);
+      }
+
+      // ignore others
+      const list = allPlugins
+        .filter(plg => plg.name === plugin.name)
+        .filter(plg => plg.importPath !== plugin.importPath)
+        .map(plg => ({ path: plg.importPath }));
+      unit.ignore?.push(...list);
+
+      pluginMap.set(plugin.name, unit);
+    }
+
+    return pluginMap;
+  }
+
+  @DefineConfigHandle('framework-config')
+  async getFrameworkConfig(
+    env?: string,
+    key = 'app',
+    frameworkMap = new Map<string, EnvUnit>()): Promise<Map<string, EnvUnit>> {
+    if (!this.frameworks.has(key)) {
+      return frameworkMap;
+    }
+    const currentEnv = env ?? process.env[ARTUS_SERVER_ENV] ?? ARTUS_DEFAULT_CONFIG_ENV.DEV;
+    const list = this.frameworks.get(key) as unknown as EnvUnit[];
+    const envList = list.filter(item => item.choose === currentEnv);
+    const defaultList = list.filter(item => item.choose === ARTUS_DEFAULT_CONFIG_ENV.DEFAULT);
+    const result = envList.length ? envList[0] : defaultList.length ? defaultList[0] : undefined;
+    if (!result) {
+      return frameworkMap;
+    }
+    result.ignore = list.filter(item => item.path !== result.path);
+    frameworkMap.set(key, result);
+    this.getFrameworkConfig(currentEnv, result?.path, frameworkMap);
+    return frameworkMap;
   }
 
-  addFramework(source: string, framework: Framework) {
-    const list = this.frameworks.get(source) ?? [];
+  addFramework(source: string, framework: EnvUnit, options: FrameworkOptions) {
+    const key = options.unitName || source;
+    const list = this.frameworks.get(key) ?? [];
+    framework.choose = options.env;
     list.push(framework);
-    this.frameworks.set(source, list);
+    this.frameworks.set(key, list);
   }
 
   getPackages(): Map<string, PackageObject[]> {
diff --git a/src/constraints.ts b/src/constraints.ts
index 99946fb..e4f995e 100644
--- a/src/constraints.ts
+++ b/src/constraints.ts
@@ -1,11 +1,13 @@
 export const DEFAULT_LOADER = 'module';
 
+export const ArtusInjectPrefix = 'artus#';
+
 export const enum ArtusInjectEnum {
   Application = 'artus#application',
   Trigger = 'artus#trigger',
   LifecycleManager = 'artus#lifecycle-manager',
   Config = 'artus#config',
-  Frameworks = 'artus#frameworks',
+  Frameworks = 'artus#framework-config',
   Packages = 'artus#packages',
   DefaultContainerName = 'artus#default_container',
 }
@@ -26,6 +28,7 @@ export const HOOK_CONSTRUCTOR_PARAMS_APP = 'appHook:constructorParams:app';
 export const HOOK_CONSTRUCTOR_PARAMS_CONTAINER = 'appHook:constructorParams:container';
 export const HOOK_PARAMS_CONTEXT = 'appHook:constructorParams:context';
 export const HOOK_FILE_LOADER = 'appHook:fileLoader';
+export const HOOK_CONFIG_HANDLE = 'appHook:configHandle::';
 
 export const DEFAULT_EXCLUDES = [
   'test',
@@ -52,7 +55,7 @@ export const DEFAULT_LOADER_LIST_WITH_ORDER = [
   'config',
   'plugin-meta',
   'module',
-  'framework',
+  'framework-config',
   'package-json',
 ];
 
diff --git a/src/loader/decorator.ts b/src/loader/decorator.ts
index baaef33..300d8bb 100644
--- a/src/loader/decorator.ts
+++ b/src/loader/decorator.ts
@@ -1,4 +1,4 @@
-import { LoaderFactory } from '.';
+import { LoaderFactory } from './factory';
 import { LoaderConstructor } from './types';
 
 export const DefineLoader = (loaderName: string): ClassDecorator => 
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index f720bdc..cb43ced 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,34 +1,82 @@
 import { Container } from '@artus/injection';
-import { ArtusInjectEnum, DEFAULT_LOADER } from '../constraints';
+import { ArtusInjectEnum, ArtusInjectPrefix, DEFAULT_LOADER, HOOK_CONFIG_HANDLE } from '../constraints';
 import { Manifest, ManifestItem, LoaderConstructor, LoaderHookUnit } from './types';
-import ConfigurationHandler from '../configuration';
+import ConfigurationHandler, { EnvUnit } from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 
-export const configSet = {
-  framework: new Set<string>(),
-  plugin: new Set<string>(),
-};
-
 export class LoaderFactory {
   private container: Container;
+  private envUnits: string[];
   private static loaderClazzMap: Map<string, LoaderConstructor> = new Map();
 
   static registerLoader(loaderName: string, clazz: LoaderConstructor) {
     this.loaderClazzMap.set(loaderName, clazz);
   }
 
-  constructor(container: Container) {
+  constructor(container: Container, envUnits: string[] = []) {
     this.container = container;
+    this.envUnits = [
+      ...envUnits,
+      'plugin-config',
+      'framework-config'
+    ]
+  }
+
+  static create(container: Container, envUnits: string[] = []): LoaderFactory {
+    return new LoaderFactory(container, envUnits);
   }
 
-  static create(container: Container): LoaderFactory {
-    return new LoaderFactory(container);
+  async setContainer(envUnit: string) {
+    const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
+    const propertyKey = Reflect.getMetadata(`${HOOK_CONFIG_HANDLE}${envUnit}`, ConfigurationHandler);
+    this.container.set({
+      id: `${ArtusInjectPrefix}${envUnit}`,
+      value: await configurationHandler[propertyKey]()
+    })
+  }
+
+  async loadEnvUnits(manifest: Manifest): Promise<void> {
+    const items = manifest.items.filter(item => this.envUnits.includes(item.loader ?? ''));
+
+    // set loader hook
+    const loaderHooks = {};
+    for (const envUnit of this.envUnits) {
+      await this.setContainer(envUnit);
+      loaderHooks[envUnit] = {
+        after: async () => await this.setContainer(envUnit)
+      };
+    }
+
+    await this.loadItemList(items, loaderHooks);
+  };
+
+  async filterUnusedFilesByEnv(manifest: Manifest): Promise<Manifest> {
+    const ignoreFiles: string[] = [];
+    for (const envUnit of this.envUnits) {
+      const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
+      const propertyKey = Reflect.getMetadata(`${HOOK_CONFIG_HANDLE}${envUnit}`, ConfigurationHandler);
+      const units: Map<string, EnvUnit> = await configurationHandler[propertyKey]();
+      for (const [, { ignore }] of units.entries()) {
+        if (!ignore) {
+          continue;
+        }
+        ignoreFiles.push(...ignore.map(item => item.path));
+      }
+    }
+
+    manifest.items = manifest.items.filter(item =>
+      !this.envUnits.includes(item.loader ?? '')
+      && ignoreFiles.every(ignoreFile => !item.path.startsWith(ignoreFile))
+    );
+
+    return manifest;
   }
 
   async loadManifest(manifest: Manifest): Promise<void> {
     const lifecycleManager: LifecycleManager = this.container.get(ArtusInjectEnum.LifecycleManager);
     const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
 
+    manifest = await this.filterUnusedFilesByEnv(manifest);
     await this.loadItemList(manifest.items, {
       config: {
         before: () => lifecycleManager.emitHook('configWillLoad'),
@@ -40,12 +88,6 @@ export class LoaderFactory {
           lifecycleManager.emitHook('configDidLoad');
         }
       },
-      framework: {
-        after: () => this.container.set({
-          id: ArtusInjectEnum.Frameworks,
-          value: configurationHandler.getFrameworks()
-        })
-      },
       'package-json': {
         after: () => this.container.set({
           id: ArtusInjectEnum.Packages,
diff --git a/src/loader/impl/framework.ts b/src/loader/impl/framework.ts
deleted file mode 100644
index 934be9e..0000000
--- a/src/loader/impl/framework.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Container } from '@artus/injection';
-import ConfigurationHandler from '../../configuration';
-import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader } from '../types';
-import compatibleRequire from '../../utils/compatible-require';
-
-@DefineLoader('framework')
-class FrameworkLoader implements Loader {
-  private container: Container;
-
-  constructor(container) {
-    this.container = container;
-  }
-
-  async load(item: ManifestItem) {
-    const originConfigObj = await compatibleRequire(item.path);
-    const configHandler = this.container.get(ConfigurationHandler);
-    configHandler.addFramework(item.source || 'app', originConfigObj);
-  }
-}
-
-export default FrameworkLoader;
diff --git a/src/loader/impl/frameworkConfig.ts b/src/loader/impl/frameworkConfig.ts
new file mode 100644
index 0000000..c838a24
--- /dev/null
+++ b/src/loader/impl/frameworkConfig.ts
@@ -0,0 +1,40 @@
+import { Container } from '@artus/injection';
+import ConfigurationHandler from '../../configuration';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
+import { ArtusInjectEnum, ARTUS_DEFAULT_CONFIG_ENV } from '../../constraints';
+
+@DefineLoader('framework-config')
+class FrameworkLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const originConfigObj = await compatibleRequire(item.path);
+    let [, env, extname] = item.filename.split('.');
+    if (!extname) {
+      // No env flag, set to Default
+      env = ARTUS_DEFAULT_CONFIG_ENV.DEFAULT;
+    }
+    let configObj = originConfigObj;
+    if (typeof originConfigObj === 'function') {
+      const app = this.container.get(ArtusInjectEnum.Application);
+      configObj = originConfigObj(app);
+    }
+
+    const configHandler = this.container.get(ConfigurationHandler);
+    configHandler.addFramework(
+      item.source || 'app',
+      configObj,
+      {
+        env,
+        unitName: item.unitName || ''
+      });
+  }
+}
+
+export default FrameworkLoader;
diff --git a/src/loader/impl/index.ts b/src/loader/impl/index.ts
index b6c93c6..7574e04 100644
--- a/src/loader/impl/index.ts
+++ b/src/loader/impl/index.ts
@@ -4,5 +4,5 @@ export * from './exception';
 export * from './extension';
 export * from './pluginMeta';
 export * from './pluginConfig';
-export * from './framework';
+export * from './frameworkConfig';
 export * from './package';
diff --git a/src/loader/types.ts b/src/loader/types.ts
index 2615c23..56a8dc2 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -10,6 +10,7 @@ interface ManifestItem extends Record<string, any> {
   filename: string;
   loader?: string;
   source?: string;
+  unitName?: string
 }
 
 interface LoaderHookUnit {
diff --git a/src/plugin/base.ts b/src/plugin/base.ts
index b1d096b..54d98eb 100644
--- a/src/plugin/base.ts
+++ b/src/plugin/base.ts
@@ -1,6 +1,5 @@
 import { Plugin, PluginConfigItem, PluginMetadata } from "./types";
-
-type PluginMap = Map<string, BasePlugin>;
+type PluginsMap = Map<string, BasePlugin[]>;
 
 export class BasePlugin implements Plugin {
   public name: string;
@@ -11,23 +10,31 @@ export class BasePlugin implements Plugin {
 
   constructor(name: string, configItem: PluginConfigItem) {
     this.name = name;
+
+    this.importPath = BasePlugin.checkGetPluginConfig(name, configItem);
+    this.enable = configItem.enable ?? false;
+  }
+
+  async init() { }
+
+  static checkGetPluginConfig(name: string, configItem: PluginConfigItem, shouldThrow: boolean = true): string {
     let importPath = configItem.path ?? '';
     if (configItem.package) {
       importPath = require.resolve(configItem.package);
     }
     if (!importPath) {
-      throw new Error(`Plugin ${name} need have path or package field`);
+      if (shouldThrow) {
+        throw new Error(`Plugin ${name} need have path or package field`);
+      }
+      return '';
     }
-    this.importPath = importPath;
-    this.enable = configItem.enable ?? false;
+    return importPath;
   }
 
-  async init() { }
-
-  checkDepExisted(pluginMap: PluginMap): void {
+  checkDepExisted(pluginsMap: PluginsMap) {
     for (const { name: pluginName, optional } of this.metadata.dependencies ?? []) {
-      const instance = pluginMap.get(pluginName);
-      if (!instance || !instance.enable) {
+      const instances = pluginsMap.get(pluginName) ?? [];
+      if (!instances.length || instances.every(instance => !instance.enable)) {
         if (optional) {
           // TODO: use artus logger instead
           console.warn(`Plugin ${this.name} need have optional dependence: ${pluginName}.`)
diff --git a/src/plugin/common.ts b/src/plugin/common.ts
index 7931b1b..dd71658 100644
--- a/src/plugin/common.ts
+++ b/src/plugin/common.ts
@@ -1,7 +1,7 @@
 import { Plugin } from './types';
 
 // A utils function that toplogical sort plugins
-export function topologicalSort(pluginInstanceMap: Map<string, Plugin>, pluginDepEdgeList: [string, string][]): string[] {
+export function topologicalSort(pluginInstancesMap: Map<string, Plugin[]>, pluginDepEdgeList: [string, string][]): string[] {
   const res: string[] = [];
   const indegree: Map<string, number> = new Map();
 
@@ -11,7 +11,7 @@ export function topologicalSort(pluginInstanceMap: Map<string, Plugin>, pluginDe
 
   const queue: string[] = [];
 
-  for (const [name] of pluginInstanceMap) {
+  for (const [name] of pluginInstancesMap) {
     if (!indegree.has(name)) {
       queue.push(name);
     }
diff --git a/src/plugin/factory.ts b/src/plugin/factory.ts
index cf1229a..c662cf9 100644
--- a/src/plugin/factory.ts
+++ b/src/plugin/factory.ts
@@ -10,26 +10,54 @@ export class PluginFactory {
     return pluginInstance;
   }
 
-  static async createFromConfig(config: Record<string, PluginConfigItem>): Promise<BasePlugin[]> {
-    const pluginInstanceMap: Map<string, BasePlugin> = new Map();
-    for (const [name, item] of Object.entries(config)) {
-      const pluginInstance = await PluginFactory.create(name, item);
-      if (pluginInstance.enable) {
-        pluginInstanceMap.set(name, pluginInstance);
-      }
+  static async createFromConfigList(config: Map<string, PluginConfigItem[]>): Promise<BasePlugin[]> {
+    const pluginInstancesMap: Map<string, BasePlugin[]> = new Map();
+    for (const [name, items] of config.entries()) {
+      const pluginInstance = await Promise.all(items.map(item => PluginFactory.create(name, item)));
+      pluginInstancesMap.set(name, pluginInstance);
     }
     let pluginDepEdgeList: [string, string][] = [];
     // Topological sort plugins
-    for (const [_name, pluginInstance] of pluginInstanceMap) {
-      pluginInstance.checkDepExisted(pluginInstanceMap);
-      pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
+    for (const [_name, pluginInstances] of pluginInstancesMap) {
+      for (const pluginInstance of pluginInstances) {
+        pluginInstance.checkDepExisted(pluginInstancesMap);
+        pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
+      }
     }
-    const pluginSortResult: string[] = topologicalSort(pluginInstanceMap, pluginDepEdgeList);
+    const pluginSortResult: string[] = topologicalSort(pluginInstancesMap, pluginDepEdgeList);
 
-    if (pluginSortResult.length !== pluginInstanceMap.size) {
-      const diffPlugin = [...pluginInstanceMap.keys()].filter((name) => !pluginSortResult.includes(name));
+    if (pluginSortResult.length !== pluginInstancesMap.size) {
+      const diffPlugin = [...pluginInstancesMap.keys()].filter((name) => !pluginSortResult.includes(name));
       throw new Error(`There is a cycle in the dependencies, wrong plugin is ${diffPlugin.join(',')}.`);
     }
-    return pluginSortResult.map((name) => pluginInstanceMap.get(name)!);
+
+    const result: BasePlugin[] = [];
+    for (const name of pluginSortResult) {
+      result.push(...(pluginInstancesMap.get(name) ?? []));
+    }
+    return this.filterDuplicatePlugins(result);
+  }
+
+  static async createFromConfig(config: Record<string, PluginConfigItem>): Promise<BasePlugin[]> {
+    const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
+    for (const [name, pluginConfig] of Object.entries(config)) {
+      pluginsConfig.set(name, [pluginConfig]);
+    }
+    return await this.createFromConfigList(pluginsConfig);
+  }
+
+  static filterDuplicatePlugins(plugins: BasePlugin[]): BasePlugin[] {
+    const exists: Map<string, boolean> = new Map();
+    const filtedPlugins: BasePlugin[] = [];
+    for (const plugin of plugins) {
+      const key = plugin.importPath;
+      if (exists.get(key)) {
+        continue;
+      }
+      exists.set(key, true);
+      filtedPlugins.push(plugin);
+    }
+
+    return filtedPlugins;
   }
 }
diff --git a/src/plugin/impl.ts b/src/plugin/impl.ts
index 0648430..e607690 100644
--- a/src/plugin/impl.ts
+++ b/src/plugin/impl.ts
@@ -1,6 +1,7 @@
 import path from 'path';
 import { BasePlugin } from './base';
 import { loadMetaFile } from '../utils/load-meta-file';
+import { exisis } from '../utils/fs';
 
 export class ArtusPlugin extends BasePlugin {
   async init() {
@@ -14,6 +15,12 @@ export class ArtusPlugin extends BasePlugin {
   }
 
   private async checkAndLoadMetadata() {
+    // check import path
+    if (!await exisis(this.importPath)) {
+      throw new Error(`load plugin <${this.name}> import path ${this.importPath} is not exists.`);
+    }
+
+    let find = false;
     const fileNameList = [
       'meta.yaml',
       'meta.yml',
@@ -22,16 +29,24 @@ export class ArtusPlugin extends BasePlugin {
     for (const fileName of fileNameList) {
       const metaFilePath = path.resolve(this.importPath, fileName);
       try {
+        if (!await exisis(metaFilePath)) {
+          continue;
+        }
         this.metadata = await loadMetaFile({
           path: metaFilePath,
           extname: path.extname(metaFilePath),
           filename: fileName,
         });
         this.metaFilePath = metaFilePath;
+        find = true;
         break;
-      } catch(e) {
-        // 单个文件找不到或者不合法，继续找下一个
+      } catch (e) {
+        throw new Error(`load plugin <${this.name}> failed, err: ${e}`);
       }
     }
+
+    if (!find) {
+      throw new Error(`load plugin <${this.name}> import path ${this.importPath} can't find meta file.`);
+    }
   }
 }
diff --git a/src/plugin/types.ts b/src/plugin/types.ts
index 2399f8a..8ed846b 100644
--- a/src/plugin/types.ts
+++ b/src/plugin/types.ts
@@ -28,6 +28,6 @@ export interface Plugin {
   metaFilePath: string;
 
   init(): Promise<void>;
-  checkDepExisted(map: Map<string, Plugin>): void;
+  checkDepExisted(map: Map<string, Plugin[]>): void;
   getDepEdgeList(): [string, string][];
 }
diff --git a/src/scanner/scan.ts b/src/scanner/scan.ts
index f054da7..f84061b 100644
--- a/src/scanner/scan.ts
+++ b/src/scanner/scan.ts
@@ -23,9 +23,9 @@ import {
     LoaderOptions,
 } from './types';
 import { Manifest, ManifestItem } from '../loader';
-import ConfigurationHandler from '../configuration';
-import { PluginFactory } from '../plugin';
+import { BasePlugin, PluginFactory } from '../plugin';
 import { FrameworkHandler } from '../framework';
+import { PluginConfigItem } from '../plugin/types';
 
 export class Scanner {
     private options: ScannerOptions;
@@ -59,20 +59,23 @@ export class Scanner {
         await this.walk(root, { source: 'app', baseDir: root });
 
         // 1. Calculate Plugin Load Order
-        const pluginConfigHandler = new ConfigurationHandler();
+        const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
         for (const pluginConfigFile of this.itemMap.get('plugin-config') ?? []) {
-            const pluginConfig = await compatibleRequire(pluginConfigFile.path);
-            if (pluginConfig) {
-                let [_, env, extname] = pluginConfigFile.filename.split('.');
-                if (!extname) {
-                    env = 'default';
+            const pluginConfig: Record<string, PluginConfigItem> = await compatibleRequire(pluginConfigFile.path);
+            for (const [name, config] of Object.entries(pluginConfig)) {
+                if (!config.path && !config.package) {
+                    continue;
+                }
+                const items = pluginsConfig.get(name);
+                if (Array.isArray(items)) {
+                    items.push(config);
+                    continue;
                 }
-                pluginConfigHandler.setConfig(env, pluginConfig);
+                pluginsConfig.set(name, [config]);
             }
         }
-        const mergedConfig = await pluginConfigHandler.getMergedConfig();
-        const pluginSortedList = await PluginFactory.createFromConfig(mergedConfig || {});
-        for (const plugin of pluginSortedList.reverse()) {
+        const allPlugins: BasePlugin[] = await PluginFactory.createFromConfigList(pluginsConfig);
+        for (const plugin of allPlugins.reverse()) {
             const metaList = this.itemMap.get('plugin-meta') ?? [];
             metaList.push({
                 path: plugin.metaFilePath,
@@ -87,24 +90,11 @@ export class Scanner {
 
         // 2. Scan Frameworks
         const serialize = FrameworkHandler.serialize;
-        const frameworkMap = new Map<string, boolean>();
         const frameworks: string[] = [];
-        frameworks.push(...serialize(this.itemMap.get('framework') ?? []));
+        const frameworkMap = new Map<string, boolean>();
+        frameworks.push(...serialize(this.itemMap.get('framework-config') ?? []));
         frameworks.push(...serialize(this.itemMap.get('package-json') ?? []));
-        let frameworkBaseDir = root;
-        for (const frame of frameworks) {
-            if (frameworkMap.get(frame)) {
-                continue;
-            }
-            frameworkMap.set(frame, true);
-            const frameworkConfig = await compatibleRequire(frame);
-            frameworkConfig.framework && (frameworkConfig.package = frameworkConfig.framework);
-            const baseFrameworkPath = await FrameworkHandler.handle(frameworkBaseDir, frameworkConfig);
-            baseFrameworkPath && (frameworkBaseDir = baseFrameworkPath)
-                && await this.walk(baseFrameworkPath, { source: 'framework', baseDir: baseFrameworkPath });
-            frameworks.push(...serialize(this.itemMap.get('framework') ?? []));
-            frameworks.push(...serialize(this.itemMap.get('package-json') ?? []));
-        }
+        await this.recurseFramework(frameworks, root, frameworkMap);
 
         const result: Manifest = {
             items: this.getItemsFromMap(),
@@ -115,6 +105,27 @@ export class Scanner {
         return result;
     }
 
+    private async recurseFramework(frameworks: string[], frameworkBaseDir: string, frameworkMap: Map<string, boolean>) {
+        const serialize = FrameworkHandler.serialize;
+        for (const frame of frameworks) {
+            if (frameworkMap.get(frame)) {
+                continue;
+            } frameworkMap.set(frame, true);
+            const frameworkConfig = await compatibleRequire(frame);
+            frameworkConfig.framework && (frameworkConfig.package = frameworkConfig.framework);
+            const baseFrameworkPath = await FrameworkHandler.handle(frameworkBaseDir, frameworkConfig);
+            baseFrameworkPath && await this.walk(baseFrameworkPath, {
+                source: 'framework',
+                baseDir: baseFrameworkPath,
+                unitName: baseFrameworkPath
+            });
+            await this.recurseFramework([
+                ...serialize(this.itemMap.get('framework-config') ?? []),
+                ...serialize(this.itemMap.get('package-json') ?? [])
+            ], baseFrameworkPath, frameworkMap);
+        }
+    }
+
     private async walk(
         root: string,
         { source, unitName, baseDir }: WalkOptions = {
@@ -123,6 +134,8 @@ export class Scanner {
             baseDir: ''
         }) {
         if (!existsSync(root)) {
+            // TODO: use artus logger instead
+            console.warn(`[scan->walk] ${root} is not exists.`);
             return;
         }
 
@@ -199,7 +212,7 @@ export class Scanner {
             } else if (this.isPluginConfig(filename)) {
                 return 'plugin-config';
             } else if (this.isFrameworkConfig(filename)) {
-                return 'framework';
+                return 'framework-config';
             }
         }
 
diff --git a/src/types.ts b/src/types.ts
index 4cdeb13..1c28273 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,6 +14,7 @@ export interface ApplicationLifecycle {
 
 export interface ApplicationInitOptions {
   containerName?: string;
+  envUnits?: string[];
 }
 
 export interface Application {
diff --git a/src/utils/fs.ts b/src/utils/fs.ts
new file mode 100644
index 0000000..ba94c36
--- /dev/null
+++ b/src/utils/fs.ts
@@ -0,0 +1,10 @@
+import fs from 'fs/promises';
+
+export async function exisis(path: string): Promise<boolean> {
+  try {
+    await fs.access(path);
+  } catch {
+    return false;
+  }
+  return true;
+};
\ No newline at end of file


Test patch:
diff --git a/test/config.test.ts b/test/config.test.ts
index 46781bc..75d9edf 100644
--- a/test/config.test.ts
+++ b/test/config.test.ts
@@ -4,10 +4,11 @@ import { ARTUS_SERVER_ENV } from '../src/constraints';
 describe('test/app.test.ts', () => {
   describe('app with config', () => {
     it('should config load on application', async () => {
-      process.env[ARTUS_SERVER_ENV] = 'production'
+      process.env[ARTUS_SERVER_ENV] = 'production';
       const { main } = await import('./fixtures/app-with-config/bootstrap');
       const app = await main();
       expect(app.config).toEqual({ name: 'test-for-config', test: 1, arr: [ 4, 5, 6 ] })
+      process.env[ARTUS_SERVER_ENV] = undefined;
     });
   });
 });
diff --git a/test/fixtures/artus-application/src/config/config.private.ts b/test/fixtures/artus-application/src/config/config.private.ts
new file mode 100644
index 0000000..9efc02a
--- /dev/null
+++ b/test/fixtures/artus-application/src/config/config.private.ts
@@ -0,0 +1,1 @@
+export const name = 'from application <private>';
diff --git a/test/fixtures/artus-application/src/config/plugin.default.ts b/test/fixtures/artus-application/src/config/plugin.default.ts
new file mode 100644
index 0000000..dc45e3b
--- /dev/null
+++ b/test/fixtures/artus-application/src/config/plugin.default.ts
@@ -0,0 +1,16 @@
+import path from 'path';
+
+export default {
+  mysql: {
+    enable: true,
+    path: path.resolve(__dirname, '../plugins/artus-plugin-mysql-rds')
+  },
+  redis: {
+    enable: true,
+    path: path.resolve(__dirname, '../plugins/artus-plugin-redis')
+  },
+  base: {
+    enable: true,
+    path: path.resolve(__dirname, '../plugins/artus-plugin-base')
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/config/plugin.private.ts b/test/fixtures/artus-application/src/config/plugin.private.ts
new file mode 100644
index 0000000..fac3f7d
--- /dev/null
+++ b/test/fixtures/artus-application/src/config/plugin.private.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+
+export default {
+  mysql: {
+    enable: true,
+    path: path.resolve(__dirname, '../plugins/artus-plugin-mysql-ob')
+  },
+  redis: {
+    enable: false
+  },
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/controller/conifg.ts b/test/fixtures/artus-application/src/controller/conifg.ts
index 49109e9..dd2bb96 100644
--- a/test/fixtures/artus-application/src/controller/conifg.ts
+++ b/test/fixtures/artus-application/src/controller/conifg.ts
@@ -1,5 +1,5 @@
 import { Context } from '@artus/pipeline';
-import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/foo/src/trigger/http';
+import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/bar/src';
 import { WithContext } from '../../../../../src/decorator';
 
 @HttpController()
diff --git a/test/fixtures/artus-application/src/controller/hello.ts b/test/fixtures/artus-application/src/controller/hello.ts
index 306f9c7..685067f 100644
--- a/test/fixtures/artus-application/src/controller/hello.ts
+++ b/test/fixtures/artus-application/src/controller/hello.ts
@@ -1,5 +1,5 @@
 import { Context } from '@artus/pipeline';
-import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/foo/src/trigger/http';
+import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/bar/src';
 import { WithContext } from '../../../../../src/decorator';
 
 @HttpController()
@@ -12,4 +12,22 @@ export default class Hello {
     const { params: { config } } = ctx.input;
     return { title: `Hello Artus ${config.name}` };
   }
+
+  @HttpMethod({
+    method: HTTPMethodEnum.GET,
+    path: '/get_name2'
+  })
+  async name2(@WithContext() ctx: Context) {
+    const { params: { config } } = ctx.input;
+    return { title: `Hello Artus ${config.name2}` };
+  }
+
+  @HttpMethod({
+    method: HTTPMethodEnum.GET,
+    path: '/get_name3'
+  })
+  async name3(@WithContext() ctx: Context) {
+    const { params: { config } } = ctx.input;
+    return { title: `Hello Artus ${config.name3}` };
+  }
 };
diff --git a/test/fixtures/artus-application/src/controller/plugin.ts b/test/fixtures/artus-application/src/controller/plugin.ts
new file mode 100644
index 0000000..b91faad
--- /dev/null
+++ b/test/fixtures/artus-application/src/controller/plugin.ts
@@ -0,0 +1,41 @@
+import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/bar/src';
+import { Inject } from '@artus/injection';
+import { WithContext } from '../../../../../src/decorator';
+import { ArtusApplication } from '../../../../../src';
+import { Context } from '@artus/pipeline';
+
+@HttpController()
+export default class Hello {
+  @Inject('ARTUS_MYSQL')
+  // @ts-ignore
+  private client: any;
+
+  @HttpMethod({
+    method: HTTPMethodEnum.GET,
+    path: '/plugin-mysql'
+  })
+  async getMysqlClient() {
+    return {
+      client: await this.client.getClient()
+    };
+  }
+
+  @HttpMethod({
+    method: HTTPMethodEnum.GET,
+    path: '/plugin-redis'
+  })
+  async getRedisClient(@WithContext() ctx: Context) {
+    const app: ArtusApplication = ctx.input.params.app;
+    let client;
+    try {
+      client = app.getContainer().get('ARTUS_REDIS');
+    } catch {
+
+    }
+
+    const result = client ? {
+      client: await client.getClient()
+    } : { message: 'plugin redis not enabled' };
+    return result;
+  }
+};
diff --git a/test/fixtures/artus-application/src/index.ts b/test/fixtures/artus-application/src/index.ts
index 69c98fb..5eb6462 100644
--- a/test/fixtures/artus-application/src/index.ts
+++ b/test/fixtures/artus-application/src/index.ts
@@ -1,12 +1,34 @@
-import { Manifest } from "../../../../src";
-import { FrameworkBar } from '../../frameworks/bar/src';
+import { Manifest, ArtusApplication, ArtusInjectEnum } from "../../../../src";
+import { AbstractBar } from '../../frameworks/bar/src';
+import { Inject, Injectable } from "@artus/injection"
 
-export default class MyArtusApplication extends FrameworkBar {
+@Injectable()
+export default class MyArtusApplication {
+  @Inject('ABSTRACT_BAR')
+  // @ts-ignore
+  private bar: AbstractBar;
+  @Inject(ArtusInjectEnum.Application)
+  // @ts-ignore
+  public artus: ArtusApplication;
+
+  static async instance(manifest: Manifest): Promise<MyArtusApplication> {
+    const app = new ArtusApplication();
+    await app.load(manifest);
+    const instance = app.getContainer().get(MyArtusApplication);
+    return instance;
+  }
+
+  isListening(): boolean {
+    return this.bar.isListening();
+  }
+
+  async run() {
+    await this.artus.run();
+  }
 }
 
 export async function main(manifest: Manifest) {
-  const app = new MyArtusApplication();
-  await app.load(manifest);
+  const app = await MyArtusApplication.instance(manifest);
   await app.run();
   return app;
 }
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-base/meta.yml b/test/fixtures/artus-application/src/plugins/artus-plugin-base/meta.yml
new file mode 100644
index 0000000..4ac7b94
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-base/meta.yml
@@ -0,0 +1,1 @@
+name: "base"
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-base/package.json b/test/fixtures/artus-application/src/plugins/artus-plugin-base/package.json
new file mode 100644
index 0000000..2b5db37
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-base/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "@artus/plugin-base"
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-base/src/config.default.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-base/src/config.default.ts
new file mode 100644
index 0000000..52bd877
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-base/src/config.default.ts
@@ -0,0 +1,5 @@
+export default {
+  mysql: {
+    clientName: 'mysql-ob-base'
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/meta.yml b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/meta.yml
new file mode 100644
index 0000000..63de297
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/meta.yml
@@ -0,0 +1,3 @@
+name: "mysql"
+dependencies:
+  - name: "base"
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/package.json b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/package.json
new file mode 100644
index 0000000..c5ea3c6
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "@artus/plugin-mysql-ob"
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/app.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/app.ts
new file mode 100644
index 0000000..60eccd6
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/app.ts
@@ -0,0 +1,22 @@
+import { Server } from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../../../src/types';
+import { ArtusApplication } from '../../../../../../../src';
+import Client, { MysqlConfig } from './client';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  async willReady() {
+    const mysql = this.app.getContainer().get('ARTUS_MYSQL') as Client;
+    await mysql.init(this.app.config.mysql as MysqlConfig);
+  }
+}
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/client.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/client.ts
new file mode 100644
index 0000000..38fe0a5
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/client.ts
@@ -0,0 +1,20 @@
+import { Injectable } from "@artus/injection";
+
+export interface MysqlConfig {
+  clientName: string
+}
+
+@Injectable({
+  id: 'ARTUS_MYSQL'
+})
+export default class Client {
+  private clientName: string = '';
+
+  async init(config: MysqlConfig) {
+    this.clientName = config.clientName;
+  }
+
+  async getClient(): Promise<string> {
+    return this.clientName;
+  }
+};
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/config/config.default.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/config/config.default.ts
new file mode 100644
index 0000000..3dbe803
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-ob/src/config/config.default.ts
@@ -0,0 +1,5 @@
+export default {
+  mysql: {
+    clientName: 'mysql-ob'
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/meta.yml b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/meta.yml
new file mode 100644
index 0000000..45c4f58
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/meta.yml
@@ -0,0 +1,1 @@
+name: "mysql"
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/package.json b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/package.json
new file mode 100644
index 0000000..f8da133
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "@artus/plugin-mysql-rds"
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/app.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/app.ts
new file mode 100644
index 0000000..60eccd6
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/app.ts
@@ -0,0 +1,22 @@
+import { Server } from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../../../src/types';
+import { ArtusApplication } from '../../../../../../../src';
+import Client, { MysqlConfig } from './client';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  async willReady() {
+    const mysql = this.app.getContainer().get('ARTUS_MYSQL') as Client;
+    await mysql.init(this.app.config.mysql as MysqlConfig);
+  }
+}
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/client.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/client.ts
new file mode 100644
index 0000000..38fe0a5
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/client.ts
@@ -0,0 +1,20 @@
+import { Injectable } from "@artus/injection";
+
+export interface MysqlConfig {
+  clientName: string
+}
+
+@Injectable({
+  id: 'ARTUS_MYSQL'
+})
+export default class Client {
+  private clientName: string = '';
+
+  async init(config: MysqlConfig) {
+    this.clientName = config.clientName;
+  }
+
+  async getClient(): Promise<string> {
+    return this.clientName;
+  }
+};
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/config/config.default.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/config/config.default.ts
new file mode 100644
index 0000000..9a5af95
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-mysql-rds/src/config/config.default.ts
@@ -0,0 +1,5 @@
+export default {
+  mysql: {
+    clientName: 'mysql-rds'
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-redis/meta.yml b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/meta.yml
new file mode 100644
index 0000000..d3d7052
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/meta.yml
@@ -0,0 +1,1 @@
+name: "redis"
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-redis/package.json b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/package.json
new file mode 100644
index 0000000..d690329
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "@artus/plugin-redis"
+}
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/app.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/app.ts
new file mode 100644
index 0000000..e7287df
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/app.ts
@@ -0,0 +1,22 @@
+import { Server } from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../../../src/types';
+import { ArtusApplication } from '../../../../../../../src';
+import Client, { RedisConfig } from './client';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  async willReady() {
+    const redis = this.app.getContainer().get('ARTUS_REDIS') as Client;
+    await redis.init(this.app.config.redis as RedisConfig);
+  }
+}
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/client.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/client.ts
new file mode 100644
index 0000000..6ce87f8
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/client.ts
@@ -0,0 +1,20 @@
+import { Injectable } from "@artus/injection";
+
+export interface RedisConfig {
+  clientName: string
+}
+
+@Injectable({
+  id: 'ARTUS_REDIS'
+})
+export default class Client {
+  private clientName: string = '';
+
+  async init(config: RedisConfig) {
+    this.clientName = config.clientName;
+  }
+
+  async getClient(): Promise<string> {
+    return this.clientName;
+  }
+};
\ No newline at end of file
diff --git a/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/config/config.default.ts b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/config/config.default.ts
new file mode 100644
index 0000000..fc76d5e
--- /dev/null
+++ b/test/fixtures/artus-application/src/plugins/artus-plugin-redis/src/config/config.default.ts
@@ -0,0 +1,5 @@
+export default {
+  redis: {
+    clientName: 'redis'
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/frameworks/abstract/foo/index.ts b/test/fixtures/frameworks/abstract/foo/index.ts
new file mode 100644
index 0000000..2ec403d
--- /dev/null
+++ b/test/fixtures/frameworks/abstract/foo/index.ts
@@ -0,0 +1,7 @@
+import { Trigger } from '../../../../../src';
+
+export interface AbstractFoo {
+  isListening: () => boolean
+}
+
+export interface HttpTrigger extends Trigger {}
\ No newline at end of file
diff --git a/test/fixtures/frameworks/bar/src/config/framework.default.ts b/test/fixtures/frameworks/bar/src/config/framework.default.ts
new file mode 100644
index 0000000..f30b90c
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/config/framework.default.ts
@@ -0,0 +1,5 @@
+import path from 'path';
+
+export default {
+  path: path.join(__dirname, '../../../layer/foo/foo1')
+}
diff --git a/test/fixtures/frameworks/bar/src/config/framework.private.ts b/test/fixtures/frameworks/bar/src/config/framework.private.ts
new file mode 100644
index 0000000..68cbcba
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/config/framework.private.ts
@@ -0,0 +1,5 @@
+import path from 'path';
+
+export default {
+  path: path.join(__dirname, '../../../layer/foo/foo2')
+}
diff --git a/test/fixtures/frameworks/bar/src/config/framework.ts b/test/fixtures/frameworks/bar/src/config/framework.ts
deleted file mode 100644
index 1956f85..0000000
--- a/test/fixtures/frameworks/bar/src/config/framework.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import path from 'path';
-
-export default {
-  path: path.join(__dirname, '../../../foo')
-}
diff --git a/test/fixtures/frameworks/foo/src/trigger/http.ts b/test/fixtures/frameworks/bar/src/http.ts
similarity index 71%
rename from test/fixtures/frameworks/foo/src/trigger/http.ts
rename to test/fixtures/frameworks/bar/src/http.ts
index 4764aad..71e46bd 100644
--- a/test/fixtures/frameworks/foo/src/trigger/http.ts
+++ b/test/fixtures/frameworks/bar/src/http.ts
@@ -1,10 +1,8 @@
 import 'reflect-metadata';
-import { Stream } from 'stream';
 import { Context, Next } from '@artus/pipeline';
-import { Trigger } from '../../../../../../src';
-import { DefineTrigger } from '../../../../../../src/decorator';
 import { Constructable } from '@artus/injection';
-import { HOOK_CONSTRUCTOR_PARAMS, HOOK_PARAMS_CONTEXT } from '../../../../../../src/constraints';
+import { HOOK_CONSTRUCTOR_PARAMS, HOOK_PARAMS_CONTEXT } from '../../../../../src/constraints';
+import { HttpTrigger } from '../../abstract/foo';
 import { Injectable, ScopeEnum } from '@artus/injection';
 
 export const enum HTTPMethodEnum {
@@ -32,36 +30,6 @@ export const controllerMap = new Set<ControllerMeta>();
 
 export const HOOK_HTTP_META_PREFIX = 'ARTUS#HOOK_HTTP_META_PREFIX::';
 
-@DefineTrigger()
-export default class HttpTrigger extends Trigger {
-  constructor() {
-    super();
-    // first of all
-    this.use(async (ctx: Context, next: Next) => {
-      await next();
-      await this.respond(ctx);
-    });
-  }
-
-  async respond(ctx: Context) {
-    const { res } = ctx.input.params;
-    const { data } = ctx.output;
-
-    res.status = data.status || 200;
-    const { content } = data;
-
-    if (Buffer.isBuffer(content) || typeof content === 'string') {
-      return res.end(content);
-    }
-
-    if (content instanceof Stream) {
-      return content.pipe(res);
-    }
-
-    return res.end(JSON.stringify(content));
-  }
-}
-
 export function HttpController(options?: ControllerParams): ClassDecorator {
   const prefix = options?.path ?? '';
   return (target: any) => {
@@ -85,6 +53,9 @@ export function registerController(trigger: HttpTrigger) {
     const fnMetaKeys = Reflect.getMetadataKeys(clazz);
 
     for (let key of fnMetaKeys) {
+      if (typeof key !== 'string') {
+        continue;
+      }
       if (!key.startsWith(HOOK_HTTP_META_PREFIX)) {
         continue;
       }
@@ -103,7 +74,7 @@ export function registerController(trigger: HttpTrigger) {
           const paramsMap = {
             [HOOK_PARAMS_CONTEXT]: ctx
           };
-          ctx.output.data.content = await target(...params.map((param) => paramsMap[param]));
+          ctx.output.data.content = await target.call(instance, ...params.map((param) => paramsMap[param]));
         }
         await next();
       });
diff --git a/test/fixtures/frameworks/bar/src/index.ts b/test/fixtures/frameworks/bar/src/index.ts
index 2938556..2eec702 100644
--- a/test/fixtures/frameworks/bar/src/index.ts
+++ b/test/fixtures/frameworks/bar/src/index.ts
@@ -1,5 +1,18 @@
-import { FrameworkFoo as Foo } from '../../foo/src';
+import { Inject, Injectable } from "@artus/injection"
+import { AbstractFoo } from "../../abstract/foo";
 
-export class FrameworkBar extends Foo {
+export interface AbstractBar extends AbstractFoo { };
 
+@Injectable({ id: 'ABSTRACT_BAR' })
+export default class FrameworkBar implements AbstractBar {
+  @Inject('ABSTRACT_FOO')
+  // @ts-ignore
+  private foo: AbstractFoo;
+
+  isListening() {
+    return this.foo.isListening();
+  }
 }
+
+
+export * from './http';
\ No newline at end of file
diff --git a/test/fixtures/frameworks/bar/src/lifecycle.ts b/test/fixtures/frameworks/bar/src/lifecycle.ts
new file mode 100644
index 0000000..cee108c
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/lifecycle.ts
@@ -0,0 +1,23 @@
+import { Server } from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../src/types';
+import { ArtusApplication } from '../../../../../src';
+import { registerController } from './/http';
+import { HttpTrigger } from '../../abstract/foo';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  async didLoad() {
+    // register controller
+    registerController(this.app.trigger as HttpTrigger);
+  }
+}
diff --git a/test/fixtures/frameworks/foo/package.json b/test/fixtures/frameworks/foo/package.json
deleted file mode 100644
index bde99de..0000000
--- a/test/fixtures/frameworks/foo/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "name": "foo"
-}
diff --git a/test/fixtures/frameworks/foo/src/config/config.default.ts b/test/fixtures/frameworks/foo/src/config/config.default.ts
deleted file mode 100644
index 5f95d01..0000000
--- a/test/fixtures/frameworks/foo/src/config/config.default.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const port = 3002;
-
-export const name = 'from framework: foo';
diff --git a/test/fixtures/frameworks/foo/src/index.ts b/test/fixtures/frameworks/foo/src/index.ts
deleted file mode 100644
index c8d5ed0..0000000
--- a/test/fixtures/frameworks/foo/src/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ArtusApplication } from '../../../../../src';
-import { server } from './lifecycle';
-
-export class FrameworkFoo extends ArtusApplication {
-  isListening() {
-    return server?.listening;
-  }
-}
diff --git a/test/fixtures/frameworks/layer/foo/foo1/package.json b/test/fixtures/frameworks/layer/foo/foo1/package.json
new file mode 100644
index 0000000..08b5568
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo1/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "foo1"
+}
diff --git a/test/fixtures/frameworks/layer/foo/foo1/src/config/config.default.ts b/test/fixtures/frameworks/layer/foo/foo1/src/config/config.default.ts
new file mode 100644
index 0000000..5d40f17
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/config/config.default.ts
@@ -0,0 +1,7 @@
+export const port = 3002;
+
+export const name = 'from framework: foo1';
+
+export const name2 = '[name2] from framework: foo1 [default]';
+
+export const name3 = '[name3] from framework: foo1 [default]';
diff --git a/test/fixtures/frameworks/layer/foo/foo1/src/config/config.private.ts b/test/fixtures/frameworks/layer/foo/foo1/src/config/config.private.ts
new file mode 100644
index 0000000..fb49096
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/config/config.private.ts
@@ -0,0 +1,3 @@
+export const name2 = '[name2] from framework: foo1 [private]';
+
+export const name3 = '[name3] from framework: foo1 [private]';
diff --git a/test/fixtures/frameworks/layer/foo/foo1/src/index.ts b/test/fixtures/frameworks/layer/foo/foo1/src/index.ts
new file mode 100644
index 0000000..2bde11b
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/index.ts
@@ -0,0 +1,10 @@
+import { Injectable } from "@artus/injection"
+import { ArtusApplication } from '../../../../../../../src';
+import { server } from './lifecycle';
+
+@Injectable({ id: 'ABSTRACT_FOO' })
+export default class FrameworkFoo extends ArtusApplication {
+  isListening(): boolean {
+    return server?.listening;
+  }
+}
diff --git a/test/fixtures/frameworks/foo/src/lifecycle.ts b/test/fixtures/frameworks/layer/foo/foo1/src/lifecycle.ts
similarity index 66%
rename from test/fixtures/frameworks/foo/src/lifecycle.ts
rename to test/fixtures/frameworks/layer/foo/foo1/src/lifecycle.ts
index a7358d7..6ea0e2b 100644
--- a/test/fixtures/frameworks/foo/src/lifecycle.ts
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/lifecycle.ts
@@ -1,10 +1,9 @@
 import { Server } from 'http';
 import http from 'http';
-import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../src/decorator';
-import { ApplicationLifecycle } from '../../../../../src/types';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../../../src/types';
 import { Input } from '@artus/pipeline';
-import { ArtusApplication } from '../../../../../src';
-import HttpTrigger, { registerController } from './trigger/http';
+import { ArtusApplication } from '../../../../../../../src';
 
 export let server: Server;
 
@@ -16,12 +15,6 @@ export default class ApplicationHookExtension implements ApplicationLifecycle {
     this.app = app;
   }
 
-  @ApplicationHook()
-  async didLoad() {
-    // register controller
-    registerController(this.app.trigger as HttpTrigger);
-  }
-
   @ApplicationHook()
   willReady() {
     const config = this.app.config ?? {};
@@ -29,7 +22,7 @@ export default class ApplicationHookExtension implements ApplicationLifecycle {
     server = http
       .createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
         const input = new Input();
-        input.params = { req, res, config };
+        input.params = { req, res, config, app: this.app };
         await this.app.trigger.startPipeline(input);
       })
       .listen(port);
diff --git a/test/fixtures/frameworks/layer/foo/foo1/src/trigger/http.ts b/test/fixtures/frameworks/layer/foo/foo1/src/trigger/http.ts
new file mode 100644
index 0000000..d22f1f0
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/trigger/http.ts
@@ -0,0 +1,34 @@
+import { Stream } from 'stream';
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../../../../../../src';
+import { DefineTrigger } from '../../../../../../../../src/decorator';
+
+@DefineTrigger()
+export default class HttpTrigger extends Trigger {
+  constructor() {
+    super();
+    // first of all
+    this.use(async (ctx: Context, next: Next) => {
+      await next();
+      await this.respond(ctx);
+    });
+  }
+
+  async respond(ctx: Context) {
+    const { res } = ctx.input.params;
+    const { data } = ctx.output;
+
+    res.status = data.status || 200;
+    const { content } = data;
+
+    if (Buffer.isBuffer(content) || typeof content === 'string') {
+      return res.end(content);
+    }
+
+    if (content instanceof Stream) {
+      return content.pipe(res);
+    }
+
+    return res.end(JSON.stringify(content));
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/frameworks/layer/foo/foo2/package.json b/test/fixtures/frameworks/layer/foo/foo2/package.json
new file mode 100644
index 0000000..9bf7199
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "foo2"
+}
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/config/config.default.ts b/test/fixtures/frameworks/layer/foo/foo2/src/config/config.default.ts
new file mode 100644
index 0000000..8c0798d
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/config/config.default.ts
@@ -0,0 +1,8 @@
+export const port = 3002;
+
+export const name = 'from framework: foo2';
+
+export const name2 = '[name2] from framework: foo2 [default]';
+
+export const name3 = '[name3] from framework: foo2 [default]';
+
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/config/config.private.ts b/test/fixtures/frameworks/layer/foo/foo2/src/config/config.private.ts
new file mode 100644
index 0000000..5ea55b5
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/config/config.private.ts
@@ -0,0 +1,1 @@
+export const name3 = '[name3] from framework: foo2 [private]';
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/index.ts b/test/fixtures/frameworks/layer/foo/foo2/src/index.ts
new file mode 100644
index 0000000..2bde11b
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/index.ts
@@ -0,0 +1,10 @@
+import { Injectable } from "@artus/injection"
+import { ArtusApplication } from '../../../../../../../src';
+import { server } from './lifecycle';
+
+@Injectable({ id: 'ABSTRACT_FOO' })
+export default class FrameworkFoo extends ArtusApplication {
+  isListening(): boolean {
+    return server?.listening;
+  }
+}
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/lifecycle.ts b/test/fixtures/frameworks/layer/foo/foo2/src/lifecycle.ts
new file mode 100644
index 0000000..6ea0e2b
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/lifecycle.ts
@@ -0,0 +1,35 @@
+import { Server } from 'http';
+import http from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../../../src/types';
+import { Input } from '@artus/pipeline';
+import { ArtusApplication } from '../../../../../../../src';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  willReady() {
+    const config = this.app.config ?? {};
+    const port = config.port;
+    server = http
+      .createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
+        const input = new Input();
+        input.params = { req, res, config, app: this.app };
+        await this.app.trigger.startPipeline(input);
+      })
+      .listen(port);
+  }
+
+  @ApplicationHook()
+  beforeClose() {
+    server?.close();
+  }
+}
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/trigger/http.ts b/test/fixtures/frameworks/layer/foo/foo2/src/trigger/http.ts
new file mode 100644
index 0000000..d22f1f0
--- /dev/null
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/trigger/http.ts
@@ -0,0 +1,34 @@
+import { Stream } from 'stream';
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../../../../../../src';
+import { DefineTrigger } from '../../../../../../../../src/decorator';
+
+@DefineTrigger()
+export default class HttpTrigger extends Trigger {
+  constructor() {
+    super();
+    // first of all
+    this.use(async (ctx: Context, next: Next) => {
+      await next();
+      await this.respond(ctx);
+    });
+  }
+
+  async respond(ctx: Context) {
+    const { res } = ctx.input.params;
+    const { data } = ctx.output;
+
+    res.status = data.status || 200;
+    const { content } = data;
+
+    if (Buffer.isBuffer(content) || typeof content === 'string') {
+      return res.end(content);
+    }
+
+    if (content instanceof Stream) {
+      return content.pipe(res);
+    }
+
+    return res.end(JSON.stringify(content));
+  }
+}
\ No newline at end of file
diff --git a/test/framework.test.ts b/test/framework.test.ts
index 9f4b390..63a8cc6 100644
--- a/test/framework.test.ts
+++ b/test/framework.test.ts
@@ -3,8 +3,17 @@ import { Scanner } from '../src/scanner';
 import path from 'path';
 import axios from 'axios';
 import assert from 'assert';
+import { ARTUS_SERVER_ENV } from '../src/constraints';
 
 describe('test/framework.test.ts', () => {
+  beforeEach(async function () {
+    process.env[ARTUS_SERVER_ENV] = 'private';
+  });
+
+  afterEach(async function () {
+    process.env[ARTUS_SERVER_ENV] = undefined;
+  });
+
   it('should run succeed', async () => {
     const scanner = new Scanner({
       needWriteFile: false, extensions: ['.ts', '.js', '.json'],
@@ -14,23 +23,37 @@ describe('test/framework.test.ts', () => {
     // console.log('manifest', manifest);
     const { main } = await import('./fixtures/artus-application/src');
     const app = await main(manifest);
-    assert(app.frameworks.get('app').length === 1);
-    assert(app.frameworks.get('framework').length === 1);
-    assert(app.packages.get('app').length === 1);
-    assert(app.packages.get('framework').length === 2);
+    const { path: appFrameworkPath, choose: appFrameworkChoose } = app.artus.frameworks.get('app');
+    assert(appFrameworkPath);
+    assert(appFrameworkChoose === 'default');
+    const { path: barFrameworkPath, choose: barFrameworkChoose } = app.artus.frameworks.get(appFrameworkPath);
+    assert(barFrameworkPath);
+    assert(barFrameworkChoose === 'private');
     assert(app.isListening());
-    const port = app.config?.port;
+    const port = app.artus.config?.port;
     assert(port === 3003);
     const testResponse = await axios.get(`http://127.0.0.1:${port}/home`);
     assert(testResponse.status === 200);
-    assert(testResponse.data.title === 'Hello Artus from application');
+    assert(testResponse.data.title === 'Hello Artus from application <private>');
 
-    // check config loaded succeed
+    //  check config loaded succeed
     const testResponseConfig = await axios.get(`http://127.0.0.1:${port}/config`);
     assert(testResponseConfig.status === 200);
     assert(testResponseConfig.data.message === 'get conifg succeed');
 
-    await app.close();
+    // check frameworke used as env
+    const testResponseName2 = await axios.get(`http://127.0.0.1:${port}/get_name2`);
+    assert(testResponseName2.data.title === 'Hello Artus [name2] from framework: foo2 [default]');
+    const testResponseName3 = await axios.get(`http://127.0.0.1:${port}/get_name3`);
+    assert(testResponseName3.data.title === 'Hello Artus [name3] from framework: foo2 [private]');
+
+    // check plugin
+    const testResponseName4 = await axios.get(`http://127.0.0.1:${port}/plugin-mysql`);
+    assert(testResponseName4.data.client === 'mysql-ob');
+    const testResponseName5 = await axios.get(`http://127.0.0.1:${port}/plugin-redis`);
+    assert(testResponseName5.data.message === 'plugin redis not enabled');
+
+    await app.artus.close();
     assert(!app.isListening());
 
   });
diff --git a/test/scanner.test.ts b/test/scanner.test.ts
index 1b99f97..05fcde0 100644
--- a/test/scanner.test.ts
+++ b/test/scanner.test.ts
@@ -9,17 +9,17 @@ describe('test/scanner.test.ts', () => {
         expect(manifest).toBeDefined();
         expect(manifest.items).toBeDefined();
         // console.log('manifest', manifest);
-        expect(manifest.items.length).toBe(12);
+        expect(manifest.items.length).toBe(14);
 
         expect(manifest.items.filter(item => item.loader === 'plugin-config').length).toBe(1);
-        expect(manifest.items.filter(item => item.loader === 'plugin-meta').length).toBe(1);
+        expect(manifest.items.filter(item => item.loader === 'plugin-meta').length).toBe(2);
         expect(manifest.items.filter(item => item.loader === 'exception').length).toBe(1);
-        expect(manifest.items.filter(item => item.loader === 'extension').length).toBe(2);
+        expect(manifest.items.filter(item => item.loader === 'extension').length).toBe(3);
         expect(manifest.items.filter(item => item.loader === 'config').length).toBe(1);
         expect(manifest.items.filter(item => item.loader === 'module').length).toBe(5);
 
         expect(manifest.items.filter(item => item.unitName === 'redis').length).toBe(2);
-        expect(manifest.items.filter(item => item.unitName === 'mysql').length).toBe(0);
+        expect(manifest.items.filter(item => item.unitName === 'mysql').length).toBe(2);
         expect(manifest.items.filter(item => item.source === 'app').length).toBe(10);
     });
 });