Repository: artusjs/core
Pull request #29 (2022-04-15 08:49:12)

feat: 对 Application 的运行机制及 Loader 进行重构，支持扫描器生成的 Manifest 及插件加载
- 待 scanner 合并，后续补充含 plugin 的完整应用用例
- 不涉及 framework 相关改动，其定义形式尚未合并

hyj1991: 这个 PR 合并后，framework 的那个看来还得修改下 config 最新的实现
noahziheng: > 这个 PR 合并后，framework 的那个看来还得修改下 config 最新的实现

是的，这两个有依赖
noahziheng: @hyj1991 @DuanPengfei 有较大改动，此前的疑问点均已调整完成
noahziheng: 经和 @hyj1991 @DuanPengfei @JerrysShan 的单点讨论，这条 Comment 发出时的实现（嵌套版本 Manifest，如下例）存在相当数量的 key 写死和 Core 内的计算动作，可读性和逻辑都不是很好，暂时考虑替换为最早讨论的“大数组”方案的 Plus 版本，将排序逻辑与 `Scanner` 整合，`Loader` 部分更纯粹地处理单点问题为佳，例如下：

（现行嵌套模式）

```ts
{
  "app": {
    "items": [
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/bootstrap",
        "extname": ".ts",
        "filename": "bootstrap.ts"
      },
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/controllers/hello",
        "extname": ".ts",
        "filename": "hello.ts"
      },
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/httpTrigger",
        "extname": ".ts",
        "filename": "httpTrigger.ts"
      },
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/koaApp",
        "extname": ".ts",
        "filename": "koaApp.ts"
      },
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/services/hello",
        "extname": ".ts",
        "filename": "hello.ts"
      }
    ],
    "packageJson": {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/package.json",
      "extname": ".json",
      "filename": "package.json"
    },
    "config": [
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/config/config.default",
        "extname": ".ts",
        "filename": "config.default.ts"
      }
    ],
    "pluginConfig": [
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/config/plugin.default",
        "extname": ".ts",
        "filename": "plugin.default.ts"
      }
    ],
    "exception": [
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/artus-exception.yaml",
        "extname": ".yaml",
        "filename": "artus-exception.yaml"
      }
    ],
    "extension": [
      {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/app",
        "extname": ".ts",
        "filename": "app.ts"
      }
    ]
  },
  "plugins": {
    "redis": {
      "pluginMeta": {
        "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/redis-plugin/meta",
        "extname": ".ts",
        "filename": "meta.ts"
      },
      "extension": [
        {
          "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/redis-plugin/app",
          "extname": ".ts",
          "filename": "app.ts"
        }
      ]
    }
  }
}
```

(计划调整为)

```ts
{
  "items": [
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/redis-plugin/meta",
      "extname": ".ts",
      "filename": "meta.ts",
      "loader": "plugin-meta",
      "source": "redis"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/redis-plugin/app",
      "extname": ".ts",
      "filename": "app.ts",
      "loader": "extension",
      "source": "redis"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/app",
      "extname": ".ts",
      "filename": "app.ts",
      "loader": "extension",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/config/plugin.default",
      "extname": ".ts",
      "filename": "plugin.default.ts",
      "loader": "config",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/config/config.default",
      "extname": ".ts",
      "filename": "config.default.ts",
      "loader": "config",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/artus-exception.yaml",
      "extname": ".yaml",
      "filename": "artus-exception.yaml",
      "loader": "exception",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/bootstrap",
      "extname": ".ts",
      "filename": "bootstrap.ts",
      "loader": "module",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/controllers/hello",
      "extname": ".ts",
      "filename": "hello.ts",
      "loader": "module",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/httpTrigger",
      "extname": ".ts",
      "filename": "httpTrigger.ts",
      "loader": "module",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/koaApp",
      "extname": ".ts",
      "filename": "koaApp.ts",
      "loader": "module",
      "source": "app"
    },
    {
      "path": "/Users/ssk/Code/core/test/fixtures/app-koa-with-ts/src/services/hello",
      "extname": ".ts",
      "filename": "hello.ts",
      "loader": "module",
      "source": "app"
    }
  ]
}
```

逻辑因为都已分块，预计晚上可以改完，可能的影响：

- Scanner 将确认成为内置模块，后续需要考虑扩展 @JerrysShan 
- Framework 的整合需要跟随这一调整在 Scanner 内处理 @hyj1991 
- Manifest 将影响运行流程，需要保证由 Scanner 或其扩展生成才能正常工作，经评估符合预期

cc @whxaxes @atian25 @Beace 


noahziheng: SEEME: Scanner 中对 pluginConfig 的合并处理目前与环境变量（ENV）有关，用于对 plugin.${env}.ts 和 plugin.default.ts 的合并，但初步评估此处可以优化为 manifest 环境无关，load 阶段做些过滤，逻辑需要整理，后续单独 PR 跟进

Patch:
diff --git a/jest.config.js b/jest.config.js
index df62a94..890e120 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,6 +1,11 @@
 /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
 module.exports = {
+  transform: {
+      '^.+\\.jsx?$': 'babel-jest',
+      '^.+\\.tsx?$': 'ts-jest',
+  },
   preset: 'ts-jest',
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
   testEnvironment: 'node',
   testPathIgnorePatterns: [
     '/test/fixtures/app-koa-with-ts/src/controllers'
diff --git a/package.json b/package.json
index 97f4aed..8f18bd9 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
     "@types/koa": "^2.13.4",
     "@types/minimatch": "^3.0.5",
     "axios": "^0.26.1",
+    "babel-jest": "^27.5.1",
     "egg-ci": "^1.19.0",
     "jest": "^27.5.1",
     "koa": "^2.13.4",
diff --git a/src/application.ts b/src/application.ts
index a584095..9e42a18 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -1,17 +1,14 @@
-import path from 'path';
-import { Constructable, Container } from '@artus/injection';
+import { Container } from '@artus/injection';
 import { ArtusInjectEnum } from './constraints';
-import { ArtusStdError, ExceptionHandler, initException } from './exception';
+import { ArtusStdError, ExceptionHandler } from './exception';
 import { HookFunction, LifecycleManager } from './lifecycle';
 import { LoaderFactory, Manifest } from './loader';
-import { Application, ApplicationInitOptions, ApplicationLifecycle } from './types';
+import { Application, ApplicationInitOptions } from './types';
 import Trigger from './trigger';
-
-export const appExtMap = new Set<Constructable<ApplicationLifecycle>>();
+import ConfigurationHandler from './configuration';
 
 export class ArtusApplication implements Application {
   public manifest?: Manifest;
-  public config?: Record<string, any>;
 
   protected container: Container;
   protected lifecycleManager: LifecycleManager;
@@ -23,18 +20,26 @@ export class ArtusApplication implements Application {
     this.lifecycleManager = new LifecycleManager(this, this.container);
     this.loaderFactory = LoaderFactory.create(this.container);
 
-    // Default Hook Register
-    this.registerHook('didLoad', initException);
-    this.initExtension();
-
     process.on('SIGINT', () => this.close());
     process.on('SIGTERM', () => this.close());
   }
 
+  get config(): Record<string, any> {
+    return this.container.get(ArtusInjectEnum.Config);
+  }
+
   get trigger(): Trigger {
     return this.container.get(ArtusInjectEnum.Trigger);
   }
 
+  get exceptionHandler(): ExceptionHandler {
+    return this.container.get(ExceptionHandler);
+  }
+
+  get configurationHandler(): ConfigurationHandler {
+    return this.container.get(ConfigurationHandler);
+  }
+
   // 兜底方法，不建议对外部使用
   getContainer(): Container {
     return this.container;
@@ -43,20 +48,12 @@ export class ArtusApplication implements Application {
   async loadDefaultClass() {
     // load Artus default clazz
     this.container.set({ id: ArtusInjectEnum.Application, value: this });
+    this.container.set({ id: ArtusInjectEnum.LifecycleManager, value: this.lifecycleManager });
 
-    await this.loaderFactory.loadManifest({
-      rootDir: __dirname,
-      items: [
-        {
-          loader: 'module',
-          path: path.resolve(__dirname, './trigger')
-        },
-        {
-          loader: 'module',
-          path: path.resolve(__dirname, './exception/handler')
-        }
-      ]
-    });
+    // SEEME: 暂时使用 set 进行注入，后续考虑更改为 Loader
+    this.container.set({ type: ConfigurationHandler });
+    this.container.set({ type: Trigger });
+    this.container.set({ type: ExceptionHandler });
 
     this.defaultClazzLoaded = true;
   }
@@ -69,20 +66,11 @@ export class ArtusApplication implements Application {
     // Load user manifest
     this.manifest = manifest;
 
-    await this.lifecycleManager.emitHook('configWillLoad');
-    const config = await this.loaderFactory.loadConfig(manifest);
-    this.config = config;
-    await this.lifecycleManager.emitHook('configDidLoad', config);
-
     await this.loaderFactory.loadManifest(manifest);
+
     await this.lifecycleManager.emitHook('didLoad');
-    return this;
-  }
 
-  async initExtension() {
-    for (const appExtClazz of appExtMap) {
-      this.lifecycleManager.batchRegisterHookByClass(appExtClazz);
-    }
+    return this;
   }
 
   async run() {
@@ -99,11 +87,11 @@ export class ArtusApplication implements Application {
   }
 
   throwException(code: string): void {
-    (this.container.get(ExceptionHandler) as ExceptionHandler).throw(code);
+    this.exceptionHandler.throw(code);
   }
 
   createException(code: string): ArtusStdError {
-    return (this.container.get(ExceptionHandler) as ExceptionHandler).create(code);
+    return this.exceptionHandler.create(code);
   }
 }
 
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
new file mode 100644
index 0000000..fee7276
--- /dev/null
+++ b/src/configuration/index.ts
@@ -0,0 +1,22 @@
+import { Injectable } from '@artus/injection';
+import { ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV } from '../constraints';
+import { mergeConfig } from '../loader/utils/merge';
+
+type ConfigObject = Record<string, any>;
+
+@Injectable()
+export default class ConfigurationHandler {
+  private configStore: Map<string, ConfigObject> = new Map();
+
+  getMergedConfig(env?: string): ConfigObject {
+    const currentEnv = env ?? process.env[ARTUS_SERVER_ENV] ?? ARTUS_DEFAULT_CONFIG_ENV.DEV;
+    const defaultConfig = this.configStore.get(ARTUS_DEFAULT_CONFIG_ENV.DEFAULT) ?? {};
+    const envConfig = this.configStore.get(currentEnv) ?? {};
+    return mergeConfig(defaultConfig, envConfig);
+  }
+
+  setConfig(env: string, config: ConfigObject) {
+    const storedConfig = this.configStore.get(env) ?? {};
+    this.configStore.set(env, mergeConfig(storedConfig, config));
+  }
+}
diff --git a/src/constraints.ts b/src/constraints.ts
index e07c466..123f95e 100644
--- a/src/constraints.ts
+++ b/src/constraints.ts
@@ -3,6 +3,8 @@ export const DEFAULT_LOADER = 'module';
 export const enum ArtusInjectEnum {
   Application = 'artus#application',
   Trigger = 'artus#trigger',
+  LifecycleManager = 'artus#lifecycle-manager',
+  Config = 'artus#config',
   DefaultContainerName = 'artus#default_container',
 }
 
@@ -36,6 +38,15 @@ export const DEFAULT_EXCLUDES = [
 export const PLUGIN_CONFIG_PATTERN = 'plugin.*';
 export const CONFIG_PATTERN = 'config.*';
 export const EXTENSION_PATTERN = 'app.*';
-export const PLUGIN_META = 'meta';
+export const PLUGIN_META = ['meta.json', 'meta.yaml', 'meta.yml'];
 export const PACKAGE_JSON = 'package.json';
 export const EXCEPTION_FILE = 'artus-exception.yaml';
+
+export const DEFAULT_LOADER_LIST_WITH_ORDER = [
+  'exception',
+  'plugin-config',
+  'extension',
+  'config',
+  'plugin-meta',
+  'module'
+];
diff --git a/src/decorator.ts b/src/decorator.ts
index f0bee7d..9f403cc 100644
--- a/src/decorator.ts
+++ b/src/decorator.ts
@@ -4,11 +4,10 @@ import {
   HOOK_CONSTRUCTOR_PARAMS_CONTAINER,
   HOOK_NAME_META_PREFIX
 } from './constraints';
-import { appExtMap } from './application';
 
 export function ApplicationExtension(): ClassDecorator {
-  return (target: any) => {
-    appExtMap.add(target);
+  return (_target: any) => {
+    // Ready to remove?
   };
 };
 
diff --git a/src/exception/appHook.ts b/src/exception/appHook.ts
deleted file mode 100644
index 895a4e9..0000000
--- a/src/exception/appHook.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { readFile } from 'fs/promises';
-import path from 'path';
-import yaml from 'js-yaml';
-import { ExceptionItem } from './types';
-import { ArtusStdError } from './error';
-import { HookFunction } from '../lifecycle';
-
-type ParserFunction = (content: string) => Record<string, ExceptionItem>;
-
-const YamlParser: ParserFunction = (content: string): Record<string, ExceptionItem> => {
-  return yaml.load(content, {
-    json: true
-  }) as Record<string, ExceptionItem>;
-};
-const JsonParser: ParserFunction = (content: string): Record<string, ExceptionItem> => {
-  return JSON.parse(content) as Record<string, ExceptionItem>;
-};
-const FILE_META_LIST: [string, ParserFunction][] = [
-  ['./artus-exception.yaml', YamlParser],
-  ['./artus-exception.yml', YamlParser],
-  ['./artus-exception.json', JsonParser]
-];
-
-export const initException: HookFunction = async ({ app }) => {
-  const pathList: string[] = [];
-  // TODO: 待补充读取插件列表，从插件根目录读取 exception 声明文件
-  pathList.push(path.resolve(__dirname, '../../')); // Artus 主包异常声明
-  if (app.manifest?.rootDir) {
-    pathList.push(app.manifest?.rootDir); // 应用根目录异常声明
-  }
-  for (const rootPath of pathList) {
-    for (const [filename, parserFunc] of FILE_META_LIST) {
-      const filePath = path.resolve(rootPath, filename);
-      try {
-        const content = await readFile(filePath, {
-          encoding: 'utf-8'
-        });
-        if (!content) {
-          continue;
-        }
-        const codeMap = parserFunc(content);
-        for (const [errCode, exceptionItem] of Object.entries(codeMap)) {
-          ArtusStdError.registerCode(errCode, exceptionItem);
-        }
-      } catch (error) {
-        if (error?.code === 'ENOENT') {
-          // Skip for next if sub-path is not exists.
-          continue;
-        }
-        console.warn(`[Artus-Exception] Parse CodeMap ${filename} failed: ${error.message}`);
-      }
-    }
-  }
-};
diff --git a/src/exception/error.ts b/src/exception/error.ts
index 518c7c4..57700d6 100644
--- a/src/exception/error.ts
+++ b/src/exception/error.ts
@@ -2,9 +2,9 @@ import { ARTUS_EXCEPTION_DEFAULT_LOCALE } from '../constraints';
 import { ExceptionItem } from './types';
 
 export const ErrorCodeUtils = {
-  getI18NDesc (code: string, locale?: string) {
+  getI18NDesc (codeMap: Map<string, ExceptionItem>, code: string, locale?: string) {
     const currentLocale = locale || process.env.ARTUS_ERROR_LOCALE || ARTUS_EXCEPTION_DEFAULT_LOCALE;
-    const exceptionItem = ArtusStdError.errorCodeMap.get(code);
+    const exceptionItem = codeMap.get(code);
     if (!exceptionItem) {
       return 'Unknown Error';
     }
@@ -16,22 +16,14 @@ export const ErrorCodeUtils = {
 };
 
 export class ArtusStdError extends Error {
-  static errorCodeMap: Map<string, ExceptionItem> = new Map();
-
-  static registerCode(code: string, exceptionItem: ExceptionItem): void {
-    if (this.errorCodeMap.has(code)) {
-      console.log(`[Artus-Exception] Register error-code failed, code is existed.(${code})`);
-      return;
-    }
-    this.errorCodeMap.set(code, exceptionItem);
-  }
-
   name: string = 'ArtusStdError';
   private _code: string;
+  private _codeMap: Map<string, ExceptionItem>;
   
-  constructor (code: string) {
+  constructor (code: string, codeMap: Map<string, ExceptionItem>) {
       super(`[${code}] This is Artus standard error, Please check on https://github.com/artusjs/error-code`);
       this._code = code;
+      this._codeMap = codeMap
   }
   
   get code(): string {
@@ -40,10 +32,10 @@ export class ArtusStdError extends Error {
   
   get desc(): string {
     // TODO: 待支持从 Config 获取 I18N 的 Locale
-    return ErrorCodeUtils.getI18NDesc(this._code);
+    return ErrorCodeUtils.getI18NDesc(this._codeMap, this._code);
   }
   
   get detailUrl(): string|undefined {
-    return ArtusStdError.errorCodeMap.get(this._code)?.detailUrl;
+    return this._codeMap.get(this._code)?.detailUrl;
   }
 }
\ No newline at end of file
diff --git a/src/exception/handler.ts b/src/exception/handler.ts
index 485cd46..76ccfe6 100644
--- a/src/exception/handler.ts
+++ b/src/exception/handler.ts
@@ -1,11 +1,24 @@
+import { Injectable } from '@artus/injection';
 import { ArtusStdError } from './error';
+import { ExceptionItem } from './types';
 
+@Injectable()
 export default class ExceptionHandler {
+  private errorCodeMap: Map<string, ExceptionItem> = new Map();
+
+  registerCode(code: string, exceptionItem: ExceptionItem): void {
+    if (this.errorCodeMap.has(code)) {
+      console.warn(`[Artus-Exception] Register error-code failed, code is existed.(${code})`);
+      return;
+    }
+    this.errorCodeMap.set(code, exceptionItem);
+  }
+
   throw(code: string): void {
-    throw new ArtusStdError(code);
+    throw new ArtusStdError(code, this.errorCodeMap);
   }
 
   create(code: string): ArtusStdError {
-    return new ArtusStdError(code);
+    return new ArtusStdError(code, this.errorCodeMap);
   }
 }
diff --git a/src/exception/index.ts b/src/exception/index.ts
index 0d9a67c..cbe398c 100644
--- a/src/exception/index.ts
+++ b/src/exception/index.ts
@@ -1,7 +1,6 @@
 import ExceptionHandler from './handler';
 
 export * from './error';
-export * from './appHook';
 
 export {
   ExceptionHandler
diff --git a/src/lifecycle/index.ts b/src/lifecycle/index.ts
index 2df32b8..dab544f 100644
--- a/src/lifecycle/index.ts
+++ b/src/lifecycle/index.ts
@@ -50,21 +50,21 @@ export class LifecycleManager {
     }
   }
 
-  batchRegisterHookByClass(hookClazz: Constructable<any>) {
-    const fnMetaKeys = Reflect.getMetadataKeys(hookClazz);
-    const constructorParams = Reflect.getMetadata(HOOK_CONSTRUCTOR_PARAMS, hookClazz) ?? [];
+  registerExtension(extClazz: Constructable<any>) {
+    const fnMetaKeys = Reflect.getMetadataKeys(extClazz);
+    const constructorParams = Reflect.getMetadata(HOOK_CONSTRUCTOR_PARAMS, extClazz) ?? [];
     const paramsMap = {
       [HOOK_CONSTRUCTOR_PARAMS_APP]: this.app,
       [HOOK_CONSTRUCTOR_PARAMS_CONTAINER]: this.container
     };
-    const hookClazzInstance = new hookClazz(...constructorParams.map((param) => paramsMap[param]));
+    const extClazzInstance = new extClazz(...constructorParams.map((param) => paramsMap[param]));
     for (const fnMetaKey of fnMetaKeys) {
       if (typeof fnMetaKey !== 'string' || !fnMetaKey.startsWith(HOOK_NAME_META_PREFIX)) {
         continue;
       }
-      const hookName = Reflect.getMetadata(fnMetaKey, hookClazz);
+      const hookName = Reflect.getMetadata(fnMetaKey, extClazz);
       const propertyKey = fnMetaKey.slice(HOOK_NAME_META_PREFIX.length);
-      this.registerHook(hookName, hookClazzInstance[propertyKey].bind(hookClazzInstance));
+      this.registerHook(hookName, extClazzInstance[propertyKey].bind(extClazzInstance));
     }
   }
 
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index bb7e1fe..f0d19eb 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,8 +1,8 @@
 import { Container } from '@artus/injection';
-import { ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV, DEFAULT_LOADER } from '../constraints';
-import { loadFile } from '../utils';
-import { mergeConfig } from '../utils/merge';
-import { Manifest, ManifestItem, LoaderConstructor } from './types';
+import { ArtusInjectEnum, DEFAULT_LOADER } from '../constraints';
+import { Manifest, ManifestItem, LoaderConstructor, LoaderHookUnit } from './types';
+import ConfigurationHandler from '../configuration';
+import { LifecycleManager } from '../lifecycle';
 
 export class LoaderFactory {
   private container: Container;
@@ -21,57 +21,46 @@ export class LoaderFactory {
   }
 
   async loadManifest(manifest: Manifest): Promise<void> {
-    for (const item of manifest.items) {
-      await this.loadItem(item);
-    }
-  }
+    const lifecycleManager: LifecycleManager = this.container.get(ArtusInjectEnum.LifecycleManager);
+    const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
 
-  async loadConfig(manifest: Manifest): Promise<Record<string, any>> {
-    const files = this.getTypeFiles();
-    let envConfigs = {};
-    let defaultConfig = {};
-    for (const file of files) {
-      const configFile = manifest.items.find(item => item.path.endsWith(`${file.path}.ts`));
-      if (!configFile) {
-        continue
+    await this.loadItemList(manifest.items, {
+      config: {
+        before: () => lifecycleManager.emitHook('configWillLoad'),
+        after: () => {
+          this.container.set({
+            id: ArtusInjectEnum.Config,
+            value: configurationHandler.getMergedConfig()
+          });
+          lifecycleManager.emitHook('configDidLoad');
+        }
       }
+    });
+  }
 
-      const currentConfig = await loadFile(configFile.path);
-
-      if (file.type === ARTUS_DEFAULT_CONFIG_ENV.DEFAULT) {
-        defaultConfig = mergeConfig(defaultConfig, currentConfig);
-      } else if (file.type === process.env[ARTUS_SERVER_ENV]) {
-        envConfigs = mergeConfig(envConfigs, currentConfig);
+  async loadItemList(itemList: ManifestItem[] = [], hookMap?: Record<string, LoaderHookUnit>): Promise<void> {
+    let prevLoader: string = '';
+    for (const item of itemList) {
+      const curLoader = item.loader ?? DEFAULT_LOADER;
+      if (item.loader !== prevLoader) {
+        if (prevLoader) {
+          await hookMap?.[prevLoader]?.after?.();
+        }
+        await hookMap?.[curLoader]?.before?.();
+        prevLoader = curLoader;
       }
+      await this.loadItem(item);
     }
-
-    return mergeConfig(defaultConfig, envConfigs);
-  }
-
-
-  // TODO:  暂时先忽略插件中的 config
-  getTypeFiles(fileType: string = 'config'): ManifestItem[] {
-    const files = [{
-      path: `${fileType}.${ARTUS_DEFAULT_CONFIG_ENV.DEFAULT}`,
-      type: ARTUS_DEFAULT_CONFIG_ENV.DEFAULT as string,
-      loader: "module",
-    }];
-    const env = process.env[ARTUS_SERVER_ENV]
-    if (env) {
-      files.push({
-        type: env,
-        path: `${fileType}.${env}`,
-        loader: "module",
-      });
+    if (prevLoader) {
+      await hookMap?.[prevLoader]?.after?.();
     }
-
-    return files;
   }
 
   async loadItem(item: ManifestItem): Promise<void> {
-    const LoaderClazz = LoaderFactory.loaderClazzMap.get(item.loader || DEFAULT_LOADER);
+    const loaderName = item.loader || DEFAULT_LOADER;
+    const LoaderClazz = LoaderFactory.loaderClazzMap.get(loaderName);
     if (!LoaderClazz) {
-      throw new Error(`Cannot find loader '${item.loader}'`);
+      throw new Error(`Cannot find loader '${loaderName}'`);
     }
     const loader = new LoaderClazz(this.container);
     await loader.load(item);
diff --git a/src/loader/impl/config.ts b/src/loader/impl/config.ts
new file mode 100644
index 0000000..ce2a5a8
--- /dev/null
+++ b/src/loader/impl/config.ts
@@ -0,0 +1,38 @@
+import { Container } from '@artus/injection';
+import ConfigurationHandler from '../../configuration';
+import { ArtusInjectEnum, ARTUS_DEFAULT_CONFIG_ENV } from '../../constraints';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
+
+@DefineLoader('config')
+class ConfigLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const originConfigObj = await compatibleRequire(item.path);
+    let [namespace, 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);
+    }
+    if (namespace !== 'config') {
+      configObj = {
+        [namespace]: configObj
+      };
+    }
+    const configHandler = this.container.get(ConfigurationHandler);
+    configHandler.setConfig(env, configObj);
+  }
+}
+
+export default ConfigLoader;
diff --git a/src/loader/impl/exception.ts b/src/loader/impl/exception.ts
new file mode 100644
index 0000000..8c36d58
--- /dev/null
+++ b/src/loader/impl/exception.ts
@@ -0,0 +1,29 @@
+import { Container } from '@artus/injection';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import { ExceptionItem } from '../../exception/types';
+import { ExceptionHandler } from '../../exception';
+import { loadMetaFile } from '../../utils/load-meta-file';
+
+@DefineLoader('exception')
+class ExceptionLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const exceptionHandler = this.container.get(ExceptionHandler);
+    try {
+      const codeMap: Record<string, ExceptionItem> = await loadMetaFile<Record<string, ExceptionItem>>(item);
+      for (const [errCode, exceptionItem] of Object.entries(codeMap)) {
+        exceptionHandler.registerCode(errCode, exceptionItem);
+      }
+    } catch (error) {
+      console.warn(`[Artus-Exception] Parse CodeMap ${item.path} failed: ${error.message}`);
+    }
+  }
+}
+
+export default ExceptionLoader;
diff --git a/src/loader/impl/extension.ts b/src/loader/impl/extension.ts
new file mode 100644
index 0000000..bc9f5e8
--- /dev/null
+++ b/src/loader/impl/extension.ts
@@ -0,0 +1,24 @@
+import { Constructable, Container } from '@artus/injection';
+import { ArtusInjectEnum } from '../../constraints';
+import { LifecycleManager } from '../../lifecycle';
+import { ApplicationLifecycle } from '../../types';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
+
+@DefineLoader('extension')
+class ExtensionLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const extClazz: Constructable<ApplicationLifecycle> = await compatibleRequire(item.path);
+    const lifecycleManager: LifecycleManager = this.container.get(ArtusInjectEnum.LifecycleManager);
+    lifecycleManager.registerExtension(extClazz);
+  }
+}
+
+export default ExtensionLoader;
diff --git a/src/loader/impl/index.ts b/src/loader/impl/index.ts
index 20a96c9..747a318 100644
--- a/src/loader/impl/index.ts
+++ b/src/loader/impl/index.ts
@@ -1,1 +1,6 @@
 export * from './module';
+export * from './config';
+export * from './exception';
+export * from './extension';
+export * from './pluginMeta';
+export * from './pluginConfig';
diff --git a/src/loader/impl/module.ts b/src/loader/impl/module.ts
index c636d30..b5b149c 100644
--- a/src/loader/impl/module.ts
+++ b/src/loader/impl/module.ts
@@ -1,6 +1,7 @@
 import { Container, InjectableDefinition } from '@artus/injection';
 import { DefineLoader } from '../decorator';
 import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
 
 @DefineLoader('module')
 class ModuleLoader implements Loader {
@@ -11,20 +12,15 @@ class ModuleLoader implements Loader {
   }
 
   async load(item: ManifestItem) {
-    try {
-      const moduleObj = await import(item.path);
-      const moduleClazz = moduleObj?.default ?? moduleObj;
-      const opts: Partial<InjectableDefinition> = {
-        path: item.path,
-        type: moduleClazz
-      };
-      if (item.id) {
-        opts.id = item.id;
-      }
-      this.container.set(opts);
-    } catch (error) {
-      console.error(error);
+    const moduleClazz = await compatibleRequire(item.path);
+    const opts: Partial<InjectableDefinition> = {
+      path: item.path,
+      type: moduleClazz
+    };
+    if (item.id) {
+      opts.id = item.id;
     }
+    this.container.set(opts);
   }
 }
 
diff --git a/src/loader/impl/pluginConfig.ts b/src/loader/impl/pluginConfig.ts
new file mode 100644
index 0000000..be0f61d
--- /dev/null
+++ b/src/loader/impl/pluginConfig.ts
@@ -0,0 +1,13 @@
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import ConfigLoader from './config';
+
+@DefineLoader('plugin-config')
+class PluginConfigLoader extends ConfigLoader implements Loader {
+
+  async load(item: ManifestItem) {
+    await super.load(item);
+  }
+}
+
+export default PluginConfigLoader;
diff --git a/src/loader/impl/pluginMeta.ts b/src/loader/impl/pluginMeta.ts
new file mode 100644
index 0000000..d94ee59
--- /dev/null
+++ b/src/loader/impl/pluginMeta.ts
@@ -0,0 +1,24 @@
+import { Container } from '@artus/injection';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import { loadMetaFile } from '../../utils/load-meta-file';
+import { PluginMetadata } from '../../plugin/types';
+
+@DefineLoader('plugin-meta')
+class PluginMetaLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const pluginMeta: PluginMetadata = await loadMetaFile<PluginMetadata>(item);
+    this.container.set({
+      id: `pluginMeta_${pluginMeta.name}`,
+      value: pluginMeta
+    });
+  }
+}
+
+export default PluginMetaLoader;
diff --git a/src/loader/index.ts b/src/loader/index.ts
index a616199..85564ce 100644
--- a/src/loader/index.ts
+++ b/src/loader/index.ts
@@ -16,5 +16,5 @@ export {
   Loader,
   LoaderConstructor,
   Manifest,
-  ManifestItem
+  ManifestItem,
 };
diff --git a/src/loader/types.ts b/src/loader/types.ts
index e8f536a..2615c23 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -1,18 +1,22 @@
 import { Container } from '@artus/injection';
 
-type LoaderType = 'module' | 'file' | string;
-
 interface Manifest {
-  rootDir: string;
   items: ManifestItem[];
 }
 
 interface ManifestItem extends Record<string, any> {
-  loader: LoaderType;
   path: string;
-  type?: string;
+  extname: string;
+  filename: string;
+  loader?: string;
+  source?: string;
 }
 
+interface LoaderHookUnit {
+  before?: Function,
+  after?: Function,
+};
+
 interface LoaderConstructor {
   new(container: Container): Loader;
 };
@@ -21,9 +25,9 @@ interface Loader {
 };
 
 export {
-  LoaderType,
   Manifest,
   ManifestItem,
+  LoaderHookUnit,
   LoaderConstructor,
   Loader,
 };
diff --git a/src/utils/merge.ts b/src/loader/utils/merge.ts
similarity index 82%
rename from src/utils/merge.ts
rename to src/loader/utils/merge.ts
index b72ba26..f833171 100644
--- a/src/utils/merge.ts
+++ b/src/loader/utils/merge.ts
@@ -1,5 +1,5 @@
 import deepmerge from 'deepmerge';
-import { isPlainObject } from './is';
+import { isPlainObject } from '../../utils/is';
 
 export function mergeConfig(...args) {
   /* istanbul ignore next */
diff --git a/src/plugin/base.ts b/src/plugin/base.ts
index 56f014c..b1d096b 100644
--- a/src/plugin/base.ts
+++ b/src/plugin/base.ts
@@ -7,10 +7,14 @@ export class BasePlugin implements Plugin {
   public enable: boolean;
   public importPath: string;
   public metadata: Partial<PluginMetadata> = {};
+  public metaFilePath: string = '';
 
   constructor(name: string, configItem: PluginConfigItem) {
     this.name = name;
-    const importPath = configItem.path ?? configItem.package;
+    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`);
     }
@@ -20,9 +24,9 @@ export class BasePlugin implements Plugin {
 
   async init() { }
 
-  checkPluginStatus(allPlugins: PluginMap, checks: string[], optional: boolean) {
-    for (const pluginName of checks) {
-      const instance = allPlugins.get(pluginName);
+  checkDepExisted(pluginMap: PluginMap): void {
+    for (const { name: pluginName, optional } of this.metadata.dependencies ?? []) {
+      const instance = pluginMap.get(pluginName);
       if (!instance || !instance.enable) {
         if (optional) {
           // TODO: use artus logger instead
@@ -34,12 +38,9 @@ export class BasePlugin implements Plugin {
     }
   }
 
-  checkDepExisted(map: PluginMap): void {
-    this.checkPluginStatus(map, this.metadata.dependencies ?? [], false);
-    this.checkPluginStatus(map, this.metadata.optionalDependencies ?? [], true);
-  }
-
   getDepEdgeList(): [string, string][] {
-    return this.metadata.dependencies?.map((depPluginName) => [this.name, depPluginName]) ?? [];
+    return this.metadata.dependencies
+      ?.filter(({ optional }) => !optional)
+      ?.map(({ name: depPluginName }) => [this.name, depPluginName]) ?? [];
   }
 }
diff --git a/src/plugin/common.ts b/src/plugin/common.ts
index fe3236f..7931b1b 100644
--- a/src/plugin/common.ts
+++ b/src/plugin/common.ts
@@ -29,10 +29,5 @@ export function topologicalSort(pluginInstanceMap: Map<string, Plugin>, pluginDe
       }
     }
   }
-
-  if (res.length !== pluginInstanceMap.size) {
-    const diffPlugin = [...pluginInstanceMap.keys()].filter((name) => !res.includes(name));
-    throw new Error(`There is a cycle in the dependencies, wrong plugin is ${diffPlugin.join(',')}.`);
-  }
   return res;
 }
diff --git a/src/plugin/factory.ts b/src/plugin/factory.ts
index c1240ce..57e1ec5 100644
--- a/src/plugin/factory.ts
+++ b/src/plugin/factory.ts
@@ -23,6 +23,11 @@ export class PluginFactory {
       pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
     }
     const pluginSortResult: string[] = topologicalSort(pluginInstanceMap, pluginDepEdgeList);
+
+    if (pluginSortResult.length !== pluginInstanceMap.size) {
+      const diffPlugin = [...pluginInstanceMap.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)!);
   }
 }
diff --git a/src/plugin/impl.ts b/src/plugin/impl.ts
index 3d53621..cf6e9e4 100644
--- a/src/plugin/impl.ts
+++ b/src/plugin/impl.ts
@@ -1,22 +1,40 @@
+import path from 'path';
 import { BasePlugin } from './base';
+import { loadMetaFile } from '../utils/load-meta-file';
 
 export class ArtusPlugin extends BasePlugin {
   async init() {
     if (!this.enable) {
       return;
     }
-    let pkgJson: Record<string, any>;
-    try {
-      pkgJson = await import(this.importPath + '/package.json');
-    } catch (error) {
-      throw new Error(`${this.name} is not have a package.json file`);
+    await this.checkAndLoadMetadata();
+    if (!this.metadata) {
+      throw new Error(`${this.name} is not have metadata.`);
     }
-    if (!pkgJson?.artusjsPlugin) {
-      throw new Error(`${this.name} is not an Artus plugin`);
-    }
-    this.metadata = pkgJson.artusjsPlugin;
     if (this.metadata.name !== this.name) {
       throw new Error(`${this.name} metadata invalid, name is ${this.metadata.name}`);
     }
   }
+
+  private async checkAndLoadMetadata() {
+    const fileNameList = [
+      'meta.yaml',
+      'meta.yml',
+      'meta.json',
+    ];
+    for (const fileName of fileNameList) {
+      const metaFilePath = path.resolve(this.importPath, fileName);
+      try {
+        this.metadata = await loadMetaFile({
+          path: metaFilePath,
+          extname: path.extname(metaFilePath),
+          filename: fileName,
+        });
+        this.metaFilePath = metaFilePath;
+        break;
+      } catch(e) {
+        // 单个文件找不到或者不合法，继续找下一个
+      }
+    }
+  }
 }
diff --git a/src/plugin/types.ts b/src/plugin/types.ts
index 28172e8..2399f8a 100644
--- a/src/plugin/types.ts
+++ b/src/plugin/types.ts
@@ -5,11 +5,15 @@ export const enum PluginType {
 
 export interface PluginMetadata {
   name: string;
-  dependencies?: string[];
-  optionalDependencies?: string[];
+  dependencies?: PluginDependencyItem[];
   type?: PluginType;
 }
 
+export interface PluginDependencyItem {
+  name: string;
+  optional?: boolean;
+}
+
 export interface PluginConfigItem {
   enable: boolean;
   path?: string;
@@ -21,6 +25,7 @@ export interface Plugin {
   enable: boolean;
   importPath: string;
   metadata: Partial<PluginMetadata>;
+  metaFilePath: string;
 
   init(): Promise<void>;
   checkDepExisted(map: Map<string, Plugin>): void;
diff --git a/src/scanner/scan.ts b/src/scanner/scan.ts
index 6e859ac..f8397ed 100644
--- a/src/scanner/scan.ts
+++ b/src/scanner/scan.ts
@@ -1,3 +1,4 @@
+import 'reflect-metadata';
 import *  as fs from 'fs/promises';
 import { existsSync } from 'fs';
 import * as path from 'path';
@@ -8,20 +9,23 @@ import {
     DEFAULT_EXCLUDES,
     PLUGIN_CONFIG_PATTERN,
     PLUGIN_META,
-    PACKAGE_JSON,
     EXCEPTION_FILE,
-    EXTENSION_PATTERN
+    EXTENSION_PATTERN,
+    DEFAULT_LOADER_LIST_WITH_ORDER,
+    DEFAULT_LOADER
 } from '../constraints';
 import {
     ScannerOptions,
-    ScannerItem,
-    ScannerUnit,
-    ScannerManifest
 } from './types';
+import { Manifest, ManifestItem } from '../loader';
+import ConfigurationHandler from '../configuration';
+import { PluginFactory } from '../plugin';
 
 export class Scanner {
     private options: ScannerOptions;
     private moduleExtensions: string[];
+    private itemMap: Map<string, ManifestItem[]>;
+
     constructor(options: Partial<ScannerOptions> = {}) {
         this.moduleExtensions = getDefaultExtensions();
         this.options = {
@@ -31,68 +35,55 @@ export class Scanner {
             excluded: DEFAULT_EXCLUDES.concat(options.excluded ?? []),
             extensions: this.moduleExtensions.concat(options.extensions ?? [], ['.yaml']),
         };
+        this.itemMap = new Map(DEFAULT_LOADER_LIST_WITH_ORDER.map((loaderName) => ([loaderName, []])));
     }
 
     public async scan(root: string) {
-        const manifest: ScannerManifest = {};
-        await this.scanUnit(this.options.appName!, root, manifest);
-        const { [this.options.appName]: app, ...others } = manifest;
-        const result = { app, plugins: {} };
-        Object.keys(others).forEach(name => {
-            result.plugins[name] = others[name];
-        });
+        // 0. Scan Application
+        await this.walk(root);
+    
+        // 1. Calculate Plugin Load Order
+        const pluginConfigHandler = new ConfigurationHandler();
+        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';
+            }
+            pluginConfigHandler.setConfig(env, pluginConfig);
+          }
+        }
+        const mergedConfig = await pluginConfigHandler.getMergedConfig();
+        const pluginSortedList = await PluginFactory.createFromConfig(mergedConfig || {});
+        for (const plugin of pluginSortedList.reverse()) {
+          const metaList = this.itemMap.get('plugin-meta') ?? [];
+          metaList.push({
+            path: plugin.metaFilePath,
+            extname: path.extname(plugin.metaFilePath),
+            filename: path.basename(plugin.metaFilePath),
+            loader: 'plugin-meta',
+            source: plugin.name
+          });
+          await this.walk(plugin.importPath, plugin.name);
+        }
+        const result: Manifest = {
+            items: this.getItemsFromMap(),
+        };
         if (this.options.needWriteFile) {
             this.writeFile('manifest.json', JSON.stringify(result, null, 2));
         }
         return result;
     }
 
-    private async scanUnit(appName: string, root: string, manifest: ScannerManifest) {
-        const result = await this.walk(root);
-        const packageJson = this.getPackageJson(root);
-        const pluginMeta = this.getPluginMeta(root);
-        manifest[appName] = {
-            packageJson: packageJson ? { path: packageJson, extname: '.json', filename: PACKAGE_JSON } : undefined,
-            pluginMeta: pluginMeta ? { ...pluginMeta, filename: `${PLUGIN_META}${pluginMeta.extname}` } : undefined,
-            items: this.filterArray(result.items),
-            config: this.filterArray(result.config),
-            pluginConfig: this.filterArray(result.pluginConfig),
-            exception: this.filterArray(result.exception),
-            extension: this.filterArray(result.extension),
-        };
-
-        for (const pluginItem of result.pluginConfig) {
-            const pluginConfig = compatibleRequire(pluginItem.path);
-            if (!Array.isArray(pluginConfig)) {
-                continue;
-            }
-            for (const plugin of pluginConfig) {
-                if (!manifest[plugin.name]) {
-                    let pluginPath = plugin.path;
-                    if (plugin.package) {
-                        pluginPath = require.resolve(plugin.package);
-                    }
-                    await this.scanUnit(plugin.name, pluginPath, manifest);
-                }
-            }
-        }
-    }
-
-    private async walk(root: string) {
-        const result: ScannerUnit = {
-            items: [],
-            config: [],
-            pluginConfig: [],
-            exception: [],
-            extension: [],
-        };
+    private async walk(root: string, unitName: string = this.options.appName!) {
         if (!existsSync(root)) {
-            return result;
+            return;
         }
 
         const stat = await fs.stat(root);
         if (!stat.isDirectory()) {
-            return result;
+            return;
         }
 
         const items = await fs.readdir(root);
@@ -109,43 +100,49 @@ export class Scanner {
                 if (this.exist(realPath, PLUGIN_META)) {
                     continue;
                 }
-                const subDir = await this.walk(realPath);
-                result.items = result.items.concat(subDir.items);
-                result.config = result.config.concat(subDir.config);
-                result.pluginConfig = result.pluginConfig.concat(subDir.pluginConfig);
-                result.exception = result.exception.concat(subDir.exception);
-                result.extension = result.extension.concat(subDir.extension);
+                await this.walk(realPath);
                 continue;
             }
 
             if (itemStat.isFile()) {
                 const filename = path.basename(realPath);
                 const filenameWithoutExt = path.basename(realPath, extname);
-                const item: ScannerItem = {
+                const item: ManifestItem = {
                     path: this.moduleExtensions.includes(extname) ? path.resolve(root, filenameWithoutExt) : realPath,
                     extname,
                     filename,
+                    loader: this.getLoaderName(filename),
+                    source: unitName
                 };
-
-                // TODO: 后面考虑重构通过参数传递匹配规则
-                switch (true) {
-                    case this.isConfig(filename):
-                        result.config.push(item)
-                        break;
-                    case this.isPluginConfig(filename):
-                        result.pluginConfig.push(item);
-                        break;
-                    case this.isException(filename):
-                        result.exception.push(item);
-                        break;
-                    case this.isExtension(filename):
-                        result.extension.push(item);
-                        break;
-                    default: result.items.push(item);
+                const itemList = this.itemMap.get(item.loader ?? DEFAULT_LOADER);
+                if (Array.isArray(itemList)) {
+                    itemList.unshift(item);
                 }
             }
         }
-        return result;
+    }
+
+    private getItemsFromMap(): ManifestItem[] {
+        let items: ManifestItem[] = [];
+        for (const [, unitItems] of this.itemMap) {
+            items = items.concat(unitItems);
+        }
+        return items;
+    }
+
+    private getLoaderName(filename: string): string {
+        // TODO: 后面考虑重构通过参数传递匹配规则
+        if (this.isConfig(filename)) {
+            return 'config';
+        } else if (this.isPluginConfig(filename)) {
+            return 'plugin-config';
+        } else if (this.isException(filename)) {
+            return 'exception';
+        } else if (this.isExtension(filename)) {
+            return 'extension';
+        } else {
+            return 'module';
+        }
     }
 
     /**
@@ -187,30 +184,9 @@ export class Scanner {
         return isMatch(filename, EXTENSION_PATTERN);
     }
 
-    private getPackageJson(dir: string) {
-        if (existsSync(path.resolve(dir, PACKAGE_JSON))) {
-            return path.resolve(dir, PACKAGE_JSON);
-        }
-        return undefined;
-    }
-
-    private getPluginMeta(dir: string) {
-        const extname = this.options.extensions.find(ext => {
-            return existsSync(path.resolve(dir, `${PLUGIN_META}${ext}`));
-        });
-        return extname ? { path: path.resolve(dir, PLUGIN_META), extname } : undefined;
-    }
-
-    private filterArray<T = any>(arr: T[]) {
-        if (!Array.isArray(arr) || !arr.length) {
-            return undefined;
-        }
-        return arr;
-    }
-
-    private exist(dir: string, filename: string): boolean {
-        return this.options.extensions.some(ext => {
-            return existsSync(path.resolve(dir, `${filename}${ext}`));
+    private exist(dir: string, filenames: string[]): boolean {
+        return filenames.some(filename => {
+            return existsSync(path.resolve(dir, `${filename}`));
         });
     }
 
diff --git a/src/scanner/types.ts b/src/scanner/types.ts
index f83c6f3..bb3aa52 100644
--- a/src/scanner/types.ts
+++ b/src/scanner/types.ts
@@ -1,27 +1,6 @@
-
-
 export interface ScannerOptions {
-    appName: string;
-    extensions: string[];
-    needWriteFile: boolean;
-    excluded?: string[];
+  appName: string;
+  extensions: string[];
+  needWriteFile: boolean;
+  excluded?: string[];
 }
-export interface ScannerItem {
-    path: string;
-    extname: string;
-    filename: string;
-}
-export interface ScannerUnit {
-    items: ScannerItem[];
-    packageJson?: ScannerItem;
-    pluginMeta?: ScannerItem;
-    config: ScannerItem[];
-    pluginConfig: ScannerItem[];
-    extension: ScannerItem[];
-    exception: ScannerItem[];
-
-}
-
-export interface ScannerManifest {
-    [name: string]: Partial<ScannerUnit>;
-}
\ No newline at end of file
diff --git a/src/utils/compatible-require.ts b/src/utils/compatible-require.ts
index 669411b..c86f7c2 100644
--- a/src/utils/compatible-require.ts
+++ b/src/utils/compatible-require.ts
@@ -4,17 +4,10 @@ import assert from 'assert';
  * compatible esModule require
  * @param path
  */
-export default function compatibleRequire(path) {
-  let requiredModule = require(path);
+export default async function compatibleRequire(path: string): Promise<any> {
+  let requiredModule = await import(path);
 
   assert(requiredModule, `module '${path}' exports is undefined`);
 
-  if (
-      requiredModule.__esModule === true &&
-      requiredModule.default !== void 0
-  ) {
-      requiredModule = requiredModule.default;
-  }
-
-  return requiredModule;
+  return requiredModule.default || requiredModule;
 }
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 69ef646..f280edc 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,20 +1,5 @@
-import path from 'path';
-import fs from 'fs';
-import compatibleRequire from './compatible-require';
 import minimatch from 'minimatch';
 
-export const loadFile = async (filepath: string) => {
-  try {
-    const extname = path.extname(filepath);
-    if (!extname) {
-      return fs.readFileSync(filepath);
-    }
-    return compatibleRequire(filepath);
-  } catch (err) {
-    throw err;
-  }
-}
-
 export function getDefaultExtensions() {
   return Object.keys(require.extensions)
 }
diff --git a/src/utils/load-meta-file.ts b/src/utils/load-meta-file.ts
new file mode 100644
index 0000000..d96a89b
--- /dev/null
+++ b/src/utils/load-meta-file.ts
@@ -0,0 +1,33 @@
+import yaml from 'js-yaml';
+import { readFile } from 'fs/promises';
+import { ManifestItem } from '../loader/types';
+
+type ParserFunction = <T = Record<string, any>>(content: string) => T;
+
+const YamlParser: ParserFunction = <T = Record<string, any>>(content: string) => {
+  return yaml.load(content, {
+    json: true
+  }) as T;
+};
+const JsonParser: ParserFunction = <T = Record<string, any>>(content: string) => {
+  return JSON.parse(content) as T;
+};
+
+export const loadMetaFile = async <T = Record<string, any>>(item: ManifestItem): Promise<T> => {
+  let parserFunc: ParserFunction;
+  if (item.extname === '.yaml' || item.extname === '.yml') {
+    parserFunc = YamlParser;
+  } else if (item.extname === '.json') {
+    parserFunc = JsonParser;
+  } else {
+    throw new Error(`[Artus-Loader] Unsupported file extension: ${item.extname} in ${item.path}`);
+  }
+  const content = await readFile(item.path, {
+    encoding: 'utf-8'
+  });
+  if (!content) {
+    throw new Error(`[Artus-Loader] File content is empty in ${item.path}.`);
+  }
+  const resultMap = parserFunc<T>(content);
+  return resultMap;
+};


Test patch:
diff --git a/test/config.test.ts b/test/config.test.ts
index 188637e..46781bc 100644
--- a/test/config.test.ts
+++ b/test/config.test.ts
@@ -5,7 +5,7 @@ describe('test/app.test.ts', () => {
   describe('app with config', () => {
     it('should config load on application', async () => {
       process.env[ARTUS_SERVER_ENV] = 'production'
-      const { main } = await import('./fixtures/app-with-config/app');
+      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 ] })
     });
diff --git a/test/exception.test.ts b/test/exception.test.ts
index 89f6967..8cedc83 100644
--- a/test/exception.test.ts
+++ b/test/exception.test.ts
@@ -5,22 +5,22 @@ import { ExceptionItem } from '../src/exception/types';
 
 describe('test/app.test.ts', () => {
   describe('register error code and throw', () => {
+    const exceptionHandler = new ExceptionHandler();
     const errorCode: string = 'ARTUS:TEMP_TEST';
     const exceptionItem: ExceptionItem = {
       desc: 'TEST-DESC',
       detailUrl: 'http://test.artusjs.org'
     };
-    ArtusStdError.registerCode(errorCode, exceptionItem);
-    const handler = new ExceptionHandler();
+    exceptionHandler.registerCode(errorCode, exceptionItem);
     try {
-      handler.throw(errorCode);
+      exceptionHandler.throw(errorCode);
     } catch (error) {
       assert(error instanceof ArtusStdError);
       assert(error.code === errorCode);
       assert(error.desc === exceptionItem.desc);
       assert(error.detailUrl === exceptionItem.detailUrl);
     }
-    const error = handler.create(errorCode);
+    const error = exceptionHandler.create(errorCode);
     assert(error instanceof ArtusStdError);
     assert(error.code === errorCode);
     assert(error.desc === exceptionItem.desc);
@@ -28,6 +28,7 @@ describe('test/app.test.ts', () => {
   });
 
   describe('register error code and throw, with i18n', () => {
+    const exceptionHandler = new ExceptionHandler();
     const errorCode: string = 'ARTUS:TEMP_TEST_I18N';
     const exceptionItem: ExceptionItem = {
       desc: {
@@ -36,8 +37,7 @@ describe('test/app.test.ts', () => {
       },
       detailUrl: 'http://test.artusjs.org'
     };
-    ArtusStdError.registerCode(errorCode, exceptionItem);
-    const handler = new ExceptionHandler();
+    exceptionHandler.registerCode(errorCode, exceptionItem);
     [
       undefined,
       'zh',
@@ -47,16 +47,19 @@ describe('test/app.test.ts', () => {
         process.env.ARTUS_ERROR_LOCALE = locale;
       }
       const tDesc = exceptionItem.desc[locale || 'en'];
-      assert(ErrorCodeUtils.getI18NDesc(errorCode, locale) === tDesc);
+      const tmpCodeMap: Map<string, ExceptionItem> = new Map([
+        [errorCode, exceptionItem]
+      ]);
+      assert(ErrorCodeUtils.getI18NDesc(tmpCodeMap, errorCode, locale) === tDesc);
       try {
-        handler.throw(errorCode);
+        exceptionHandler.throw(errorCode);
       } catch (error) {
         assert(error instanceof ArtusStdError);
         assert(error.code === errorCode);
         assert(error.desc === tDesc);
         assert(error.detailUrl === exceptionItem.detailUrl);
       }
-      const error = handler.create(errorCode);
+      const error = exceptionHandler.create(errorCode);
       assert(error instanceof ArtusStdError);
       assert(error.code === errorCode);
       assert(error.desc === tDesc);
@@ -69,40 +72,40 @@ describe('test/app.test.ts', () => {
       try {
         const {
           main
-        } = await import('./fixtures/exception-with-ts-yaml/app');
+        } = await import('./fixtures/exception-with-ts-yaml/bootstrap');
         const app = await main();
 
         try {
           app.throwException('ARTUS:GLOBAL_TEST');
         } catch (error) {
-          assert(error instanceof ArtusStdError);
-          assert(error.code === 'ARTUS:GLOBAL_TEST');
-          assert(error.desc === '全局测试错误，仅用于单元测试');
-          assert(error.detailUrl === 'https://github.com/artusjs/spec');
+          expect(error).toBeInstanceOf(ArtusStdError);
+          expect(error.code).toBe('ARTUS:GLOBAL_TEST');
+          expect(error.desc).toBe('全局测试错误，仅用于单元测试');
+          expect(error.detailUrl).toBe('https://github.com/artusjs/spec');
         }
         try {
           app.throwException('APP:TEST_ERROR');
         } catch (error) {
-          assert(error instanceof ArtusStdError);
-          assert(error.code === 'APP:TEST_ERROR');
-          assert(error.desc === '这是一个测试用的错误');
-          assert(error.detailUrl === 'https://github.com/artusjs');
+          expect(error).toBeInstanceOf(ArtusStdError);
+          expect(error.code).toBe('APP:TEST_ERROR');
+          expect(error.desc).toBe('这是一个测试用的错误');
+          expect(error.detailUrl).toBe('https://github.com/artusjs');
         }
         try {
           process.env.ARTUS_ERROR_LOCALE = 'en';
           app.throwException('ARTUS:GLOBAL_TEST_I18N');
         } catch (error) {
-          assert(error instanceof ArtusStdError);
-          assert(error.code === 'ARTUS:GLOBAL_TEST_I18N');
-          assert(error.desc === 'This is a test exception, only valid in unit-test');
-          assert(error.detailUrl === 'https://github.com/artusjs/spec');
+          expect(error).toBeInstanceOf(ArtusStdError);
+          expect(error.code).toBe('ARTUS:GLOBAL_TEST_I18N');
+          expect(error.desc).toBe('This is a test exception, only valid in unit-test');
+          expect(error.detailUrl).toBe('https://github.com/artusjs/spec');
         }
 
         const error = app.createException('ARTUS:GLOBAL_TEST');
-        assert(error instanceof ArtusStdError);
-        assert(error.code === 'ARTUS:GLOBAL_TEST');
-        assert(error.desc === '全局测试错误，仅用于单元测试');
-        assert(error.detailUrl === 'https://github.com/artusjs/spec');
+        expect(error).toBeInstanceOf(ArtusStdError);
+        expect(error.code).toBe('ARTUS:GLOBAL_TEST');
+        expect(error.desc).toBe('全局测试错误，仅用于单元测试');
+        expect(error.detailUrl).toBe('https://github.com/artusjs/spec');
 
         await app.close();
       } catch (error) {
diff --git a/test/fixtures/app-koa-with-ts/src/app.ts b/test/fixtures/app-koa-with-ts/src/app.ts
index b7c3ff6..37c3f4b 100644
--- a/test/fixtures/app-koa-with-ts/src/app.ts
+++ b/test/fixtures/app-koa-with-ts/src/app.ts
@@ -13,7 +13,7 @@ import HelloController from './controllers/hello';
 export let server: Server;
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
   container: Container;
 
diff --git a/test/fixtures/app-koa-with-ts/src/bootstrap.ts b/test/fixtures/app-koa-with-ts/src/bootstrap.ts
index 9eaf4f1..5f6efc0 100644
--- a/test/fixtures/app-koa-with-ts/src/bootstrap.ts
+++ b/test/fixtures/app-koa-with-ts/src/bootstrap.ts
@@ -5,23 +5,41 @@ import { server } from './app';
 async function main() {
   const app: ArtusApplication = new ArtusApplication();
   await app.load({
-    rootDir: __dirname,
     items: [
       {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, './koaApp'),
+        extname: '.ts',
+        filename: 'koaApp.ts',
         loader: 'module',
-        path: path.resolve(__dirname, './koaApp')
+        source: 'app'
       },
       {
+        path: path.resolve(__dirname, './httpTrigger'),
+        extname: '.ts',
+        filename: 'httpTrigger.ts',
         loader: 'module',
-        path: path.resolve(__dirname, './httpTrigger')
+        source: 'app'
       },
       {
+        path: path.resolve(__dirname, './controllers/hello'),
+        extname: '.ts',
+        filename: 'hello.ts',
         loader: 'module',
-        path: path.resolve(__dirname, './controllers/hello')
+        source: 'app'
       },
       {
+        path: path.resolve(__dirname, './services/hello'),
+        extname: '.ts',
+        filename: 'hello.ts',
         loader: 'module',
-        path: path.resolve(__dirname, './services/hello')
+        source: 'app'
       }
     ]
   });
diff --git a/test/fixtures/app-koa-with-ts/src/config/config.default.ts b/test/fixtures/app-koa-with-ts/src/config/config.default.ts
index 9f4e203..f4b1c92 100644
--- a/test/fixtures/app-koa-with-ts/src/config/config.default.ts
+++ b/test/fixtures/app-koa-with-ts/src/config/config.default.ts
@@ -1,5 +1,3 @@
-
-
 export default {
-    name: 'artus'
-}
\ No newline at end of file
+  name: 'artus'
+};
diff --git a/test/fixtures/app-koa-with-ts/src/config/plugin.default.ts b/test/fixtures/app-koa-with-ts/src/config/plugin.default.ts
index 93bf125..922124c 100644
--- a/test/fixtures/app-koa-with-ts/src/config/plugin.default.ts
+++ b/test/fixtures/app-koa-with-ts/src/config/plugin.default.ts
@@ -1,5 +1,8 @@
-
 import path from 'path';
-export default [
-    { name: 'redis', path: path.resolve(__dirname, '../redis-plugin') },
-]
\ No newline at end of file
+
+export default {
+  redis: {
+    enable: true,
+    path: path.resolve(__dirname, '../redis-plugin')
+  },
+};
diff --git a/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.ts b/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.ts
deleted file mode 100644
index 96cc084..0000000
--- a/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-
-export default {
-    name: "redis",
-    dependencies: []
-}
\ No newline at end of file
diff --git a/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.yaml b/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.yaml
new file mode 100644
index 0000000..8a1d960
--- /dev/null
+++ b/test/fixtures/app-koa-with-ts/src/redis-plugin/meta.yaml
@@ -0,0 +1,1 @@
+name: redis
diff --git a/test/fixtures/app-with-config/app.ts b/test/fixtures/app-with-config/app.ts
index 7aa0b74..0b44664 100644
--- a/test/fixtures/app-with-config/app.ts
+++ b/test/fixtures/app-with-config/app.ts
@@ -1,17 +1,13 @@
-import path from 'path';
 import { Server } from 'http';
 import Koa from 'koa';
-import { ArtusApplication } from '../../../src';
 import { ApplicationExtension, ApplicationHook } from '../../../src/decorator';
 import { ApplicationLifecycle } from '../../../src/types';
 
-const rootDir = path.resolve(__dirname, './');
-
 let server: Server;
 const koaApp = new Koa();
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   testStr: string = 'Hello Artus';
 
   @ApplicationHook()
@@ -28,26 +24,3 @@ export class ApplicationHookExtension implements ApplicationLifecycle {
     server?.close();
   }
 }
-
-async function main() {
-  const app = new ArtusApplication();
-  await app.load({
-    rootDir,
-    items: [
-      {
-        loader: 'module',
-        path: path.resolve(rootDir, './config/config.default.ts')
-      },
-      {
-        loader: 'module',
-        path: path.resolve(rootDir, './config/config.production.ts')
-      }
-    ]
-  });
-  return app;
-};
-
-
-export {
-  main,
-};
diff --git a/test/fixtures/app-with-config/bootstrap.ts b/test/fixtures/app-with-config/bootstrap.ts
new file mode 100644
index 0000000..1fd7a2c
--- /dev/null
+++ b/test/fixtures/app-with-config/bootstrap.ts
@@ -0,0 +1,39 @@
+import path from 'path';
+import { ArtusApplication } from '../../../src';
+
+const rootDir = path.resolve(__dirname, './');
+
+async function main() {
+  const app = new ArtusApplication();
+  await app.load({
+    items: [
+      {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(rootDir, './config/config.default'),
+        extname: '.ts',
+        filename: 'config.default.ts',
+        loader: 'config',
+        source: 'app'
+      },
+      {
+        path: path.resolve(rootDir, './config/config.production'),
+        extname: '.ts',
+        filename: 'config.production.ts',
+        loader: 'config',
+        source: 'app'
+      },
+    ]
+  });
+  return app;
+};
+
+
+export {
+  main,
+};
diff --git a/test/fixtures/exception-with-ts-yaml/app.ts b/test/fixtures/exception-with-ts-yaml/app.ts
index e439949..6ee282c 100644
--- a/test/fixtures/exception-with-ts-yaml/app.ts
+++ b/test/fixtures/exception-with-ts-yaml/app.ts
@@ -1,14 +1,13 @@
 import { Server } from 'http';
 import Koa from 'koa';
-import { ArtusApplication } from '../../../src';
 import { ApplicationExtension, ApplicationHook } from '../../../src/decorator';
 import { ApplicationLifecycle } from '../../../src/types';
 
-let server: Server;
+export let server: Server;
 const koaApp = new Koa();
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   testStr: string = 'Hello Artus';
 
   @ApplicationHook()
@@ -25,20 +24,3 @@ export class ApplicationHookExtension implements ApplicationLifecycle {
     server?.close();
   }
 }
-
-async function main() {
-  const app = new ArtusApplication();
-  await app.load({
-    rootDir: __dirname,
-    items: []
-  });
-  await app.run();
-  return app;
-};
-
-const isListening = () => server.listening;
-
-export {
-  main,
-  isListening
-};
diff --git a/test/fixtures/exception-with-ts-yaml/bootstrap.ts b/test/fixtures/exception-with-ts-yaml/bootstrap.ts
new file mode 100644
index 0000000..fd11c9c
--- /dev/null
+++ b/test/fixtures/exception-with-ts-yaml/bootstrap.ts
@@ -0,0 +1,41 @@
+import path from 'path';
+import { ArtusApplication } from '../../../src';
+import { server } from './app';
+
+async function main() {
+  const app = new ArtusApplication();
+  await app.load({
+    items: [
+      {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, '../../../artus-exception.yaml'),
+        extname: '.yaml',
+        filename: 'artus-exception.yaml',
+        loader: 'exception',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, './artus-exception.yaml'),
+        extname: '.yaml',
+        filename: 'artus-exception.yaml',
+        loader: 'exception',
+        source: 'app'
+      }
+    ]
+  });
+  await app.run();
+  return app;
+};
+
+const isListening = () => server.listening;
+
+export {
+  main,
+  isListening
+};
diff --git a/test/fixtures/module-with-js/src/index.js b/test/fixtures/module-with-js/src/index.js
index edc8041..a46e8f8 100644
--- a/test/fixtures/module-with-js/src/index.js
+++ b/test/fixtures/module-with-js/src/index.js
@@ -3,18 +3,19 @@ const path = require('path');
 const rootDir = path.resolve(__dirname, './');
 
 module.exports = ({
-  rootDir,
   items: [
     {
-      loader: 'module',
       id: 'testServiceA',
-      path: path.resolve(rootDir, './testServiceA.js')
+      path: path.resolve(rootDir, './testServiceA.js'),
+      extname: '.js',
+      filename: 'testServiceA.js',
     },
     {
-      loader: 'module',
       id: 'testServiceB',
       scope: 'Execution',
-      path: path.resolve(rootDir, './testServiceB.js')
+      path: path.resolve(rootDir, './testServiceB.js'),
+      extname: '.js',
+      filename: 'testServiceB.js',
     }
   ]
 });
diff --git a/test/fixtures/module-with-ts/src/index.ts b/test/fixtures/module-with-ts/src/index.ts
index 9af1be3..75691bd 100644
--- a/test/fixtures/module-with-ts/src/index.ts
+++ b/test/fixtures/module-with-ts/src/index.ts
@@ -1,17 +1,19 @@
 import path from 'path';
+import { Manifest } from '../../../../src';
 
 const rootDir = path.resolve(__dirname, './');
 
 export default ({
-  rootDir,
   items: [
     {
-      loader: 'module',
-      path: path.resolve(rootDir, './testServiceA.ts')
+      path: path.resolve(rootDir, './testServiceA.ts'),
+      extname: '.ts',
+      filename: 'testServiceA.ts',
     },
     {
-      loader: 'module',
-      path: path.resolve(rootDir, './testServiceB.ts')
+      path: path.resolve(rootDir, './testServiceB.ts'),
+      extname: '.ts',
+      filename: 'testServiceB.ts',
     }
   ]
-});
+}) as Manifest;
diff --git a/test/fixtures/plugins/plugin-a/meta.yaml b/test/fixtures/plugins/plugin-a/meta.yaml
new file mode 100644
index 0000000..7058d13
--- /dev/null
+++ b/test/fixtures/plugins/plugin-a/meta.yaml
@@ -0,0 +1,5 @@
+name: "plugin-a"
+dependencies:
+  - name: "plugin-b"
+  - name: "plugin-c"
+    optional: true
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-a/package.json b/test/fixtures/plugins/plugin-a/package.json
index 012c236..99c86d4 100644
--- a/test/fixtures/plugins/plugin-a/package.json
+++ b/test/fixtures/plugins/plugin-a/package.json
@@ -1,8 +1,3 @@
 {
-  "name": "@artus/test-plugin-a",
-  "artusjsPlugin": {
-    "name": "plugin-a",
-    "dependencies": [ "plugin-b" ],
-    "optionalDependencies": [ "plugin-c" ]
-  }
+  "name": "@artus/test-plugin-a"
 }
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-b/meta.yaml b/test/fixtures/plugins/plugin-b/meta.yaml
new file mode 100644
index 0000000..687afb7
--- /dev/null
+++ b/test/fixtures/plugins/plugin-b/meta.yaml
@@ -0,0 +1,3 @@
+name: "plugin-b"
+dependencies:
+  - name: "plugin-c"
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-b/package.json b/test/fixtures/plugins/plugin-b/package.json
index 4d2940d..27aeee5 100644
--- a/test/fixtures/plugins/plugin-b/package.json
+++ b/test/fixtures/plugins/plugin-b/package.json
@@ -1,7 +1,3 @@
 {
-  "name": "@artus/test-plugin-b",
-  "artusjsPlugin": {
-    "name": "plugin-b",
-    "dependencies": [ "plugin-c" ]
-  }
+  "name": "@artus/test-plugin-b"
 }
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-c/meta.yaml b/test/fixtures/plugins/plugin-c/meta.yaml
new file mode 100644
index 0000000..9fd3090
--- /dev/null
+++ b/test/fixtures/plugins/plugin-c/meta.yaml
@@ -0,0 +1,1 @@
+name: "plugin-c"
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-c/package.json b/test/fixtures/plugins/plugin-c/package.json
index eef8fd4..6286a10 100644
--- a/test/fixtures/plugins/plugin-c/package.json
+++ b/test/fixtures/plugins/plugin-c/package.json
@@ -1,6 +1,3 @@
 {
-  "name": "@artus/test-plugin-c",
-  "artusjsPlugin": {
-    "name": "plugin-c"
-  }
+  "name": "@artus/test-plugin-c"
 }
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-d/meta.yaml b/test/fixtures/plugins/plugin-d/meta.yaml
new file mode 100644
index 0000000..8d10c84
--- /dev/null
+++ b/test/fixtures/plugins/plugin-d/meta.yaml
@@ -0,0 +1,4 @@
+name: "plugin-d"
+dependencies:
+  - name: "plugin-e"
+    optional: true
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-d/package.json b/test/fixtures/plugins/plugin-d/package.json
index 158b276..a49dbc9 100644
--- a/test/fixtures/plugins/plugin-d/package.json
+++ b/test/fixtures/plugins/plugin-d/package.json
@@ -1,9 +1,3 @@
 {
-  "name": "@artus/test-plugin-d",
-  "artusjsPlugin": {
-    "name": "plugin-d",
-    "optionalDependencies": [
-      "plugin-e"
-    ]
-  }
+  "name": "@artus/test-plugin-d"
 }
diff --git a/test/fixtures/plugins/plugin-wrong-a/meta.yaml b/test/fixtures/plugins/plugin-wrong-a/meta.yaml
new file mode 100644
index 0000000..2e6c198
--- /dev/null
+++ b/test/fixtures/plugins/plugin-wrong-a/meta.yaml
@@ -0,0 +1,3 @@
+name: "plugin-wrong-a"
+dependencies:
+  - name: "plugin-wrong-b"
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-wrong-a/package.json b/test/fixtures/plugins/plugin-wrong-a/package.json
index 6fe0357..0687290 100644
--- a/test/fixtures/plugins/plugin-wrong-a/package.json
+++ b/test/fixtures/plugins/plugin-wrong-a/package.json
@@ -1,7 +1,3 @@
 {
-  "name": "@artus/test-plugin-wrong-a",
-  "artusjsPlugin": {
-    "name": "plugin-wrong-a",
-    "dependencies": [ "plugin-wrong-b" ]
-  }
+  "name": "@artus/test-plugin-wrong-a"
 }
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-wrong-b/meta.yaml b/test/fixtures/plugins/plugin-wrong-b/meta.yaml
new file mode 100644
index 0000000..a8a9138
--- /dev/null
+++ b/test/fixtures/plugins/plugin-wrong-b/meta.yaml
@@ -0,0 +1,3 @@
+name: "plugin-wrong-b"
+dependencies:
+  - name: "plugin-wrong-a"
\ No newline at end of file
diff --git a/test/fixtures/plugins/plugin-wrong-b/package.json b/test/fixtures/plugins/plugin-wrong-b/package.json
index e76b335..6b0e371 100644
--- a/test/fixtures/plugins/plugin-wrong-b/package.json
+++ b/test/fixtures/plugins/plugin-wrong-b/package.json
@@ -1,7 +1,3 @@
 {
-  "name": "@artus/test-plugin-wrong-b",
-  "artusjsPlugin": {
-    "name": "plugin-wrong-b",
-    "dependencies": [ "plugin-wrong-a" ]
-  }
+  "name": "@artus/test-plugin-wrong-b"
 }
\ No newline at end of file
diff --git a/test/fixtures/trigger/event/app.ts b/test/fixtures/trigger/event/app.ts
index 386bcd8..d90d701 100644
--- a/test/fixtures/trigger/event/app.ts
+++ b/test/fixtures/trigger/event/app.ts
@@ -1,14 +1,13 @@
-import  path from 'path';
 import { EventEmitter } from 'events';
 import { ArtusApplication } from '../../../../src';
 import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../src/decorator';
 import { Context, Input, Next } from '@artus/pipeline';
 import { ApplicationLifecycle } from '../../../../src/types';
 
-let event = new EventEmitter();
+export let event = new EventEmitter();
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
   constructor(@WithApplication() app: ArtusApplication) {
@@ -60,28 +59,3 @@ export class ApplicationHookExtension implements ApplicationLifecycle {
     event.removeAllListeners();
   }
 }
-
-async function main() {
-  const app: ArtusApplication = new ArtusApplication();
-  await app.load({
-    rootDir: __dirname,
-    items: [
-      {
-        loader: 'module',
-        path: path.resolve(__dirname, './eventTrigger')
-      }
-    ]
-  });
-  await app.run();
-
-  return app;
-};
-
-function pub(e: 'e1' | 'e2', p: any) {
-  event.emit(e, p);
-}
-
-export {
-  main,
-  pub
-};
diff --git a/test/fixtures/trigger/event/bootstrap.ts b/test/fixtures/trigger/event/bootstrap.ts
new file mode 100644
index 0000000..c04d13f
--- /dev/null
+++ b/test/fixtures/trigger/event/bootstrap.ts
@@ -0,0 +1,37 @@
+import  path from 'path';
+import { ArtusApplication } from '../../../../src';
+import { event } from './app';
+
+async function main() {
+  const app: ArtusApplication = new ArtusApplication();
+  await app.load({
+    items: [
+      {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, './eventTrigger'),
+        extname: '.ts',
+        filename: 'eventTrigger.ts',
+        loader: 'module',
+        source: 'app'
+      }
+    ]
+  });
+  await app.run();
+
+  return app;
+};
+
+function pub(e: 'e1' | 'e2', p: any) {
+  event.emit(e, p);
+}
+
+export {
+  main,
+  pub
+};
diff --git a/test/fixtures/trigger/http/app.ts b/test/fixtures/trigger/http/app.ts
index 47a4e1b..caf28f0 100644
--- a/test/fixtures/trigger/http/app.ts
+++ b/test/fixtures/trigger/http/app.ts
@@ -1,15 +1,13 @@
-import path from 'path';
-import { Server } from 'http';
+import http, { Server } from 'http';
+import { Context, Input } from '@artus/pipeline';
 import { ArtusApplication } from '../../../../src';
 import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../src/decorator';
-import http from 'http';
-import { Context, Input } from '@artus/pipeline';
 import { ApplicationLifecycle } from '../../../../src/types';
 
-let server: Server;
+export let server: Server;
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
   constructor(@WithApplication() app: ArtusApplication) {
@@ -40,26 +38,3 @@ export class ApplicationHookExtension implements ApplicationLifecycle {
     server?.close();
   }
 }
-
-async function main() {
-  const app: ArtusApplication = new ArtusApplication();
-  await app.load({
-    rootDir: __dirname,
-    items: [
-      {
-        loader: 'module',
-        path: path.resolve(__dirname, './httpTrigger')
-      }
-    ]
-  });
-  await app.run();
-
-  return app;
-};
-
-const isListening = () => server.listening;
-
-export {
-  main,
-  isListening
-};
diff --git a/test/fixtures/trigger/http/bootstrap.ts b/test/fixtures/trigger/http/bootstrap.ts
new file mode 100644
index 0000000..d93ef35
--- /dev/null
+++ b/test/fixtures/trigger/http/bootstrap.ts
@@ -0,0 +1,35 @@
+import path from 'path';
+import { ArtusApplication } from '../../../../src';
+import { server } from './app';
+
+async function main() {
+  const app: ArtusApplication = new ArtusApplication();
+  await app.load({
+    items: [
+      {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, './httpTrigger'),
+        extname: '.ts',
+        filename: 'httpTrigger.ts',
+        loader: 'module',
+        source: 'app'
+      }
+    ]
+  });
+  await app.run();
+
+  return app;
+};
+
+const isListening = () => server.listening;
+
+export {
+  main,
+  isListening
+};
diff --git a/test/fixtures/trigger/timer/app.ts b/test/fixtures/trigger/timer/app.ts
index 41526df..d670fe7 100644
--- a/test/fixtures/trigger/timer/app.ts
+++ b/test/fixtures/trigger/timer/app.ts
@@ -1,11 +1,10 @@
-import path from 'path';
 import { ArtusApplication } from '../../../../src';
 import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../src/decorator';
 import { Context, Input } from '@artus/pipeline';
 import { ApplicationLifecycle } from '../../../../src/types';
 
 let timers: any[] = [];
-let execution = {
+export let execution = {
   task1: {
     count: 0,
     cost: 0,
@@ -17,7 +16,7 @@ let execution = {
 };
 
 @ApplicationExtension()
-export class ApplicationHookExtension implements ApplicationLifecycle {
+export default class ApplicationHookExtension implements ApplicationLifecycle {
   app: ArtusApplication;
 
   constructor(@WithApplication() app: ArtusApplication) {
@@ -63,28 +62,3 @@ export class ApplicationHookExtension implements ApplicationLifecycle {
     timers.forEach(clearInterval);
   }
 }
-
-async function main() {
-  const app: ArtusApplication = new ArtusApplication();
-  await app.load({
-    rootDir: __dirname,
-    items: [
-      {
-        loader: 'module',
-        path: path.resolve(__dirname, './timerTrigger')
-      }
-    ]
-  });
-  await app.run();
-
-  return app;
-};
-
-function getTaskExecution() {
-  return execution;
-}
-
-export {
-  main,
-  getTaskExecution
-};
diff --git a/test/fixtures/trigger/timer/bootstrap.ts b/test/fixtures/trigger/timer/bootstrap.ts
new file mode 100644
index 0000000..2367e3a
--- /dev/null
+++ b/test/fixtures/trigger/timer/bootstrap.ts
@@ -0,0 +1,37 @@
+import path from 'path';
+import { ArtusApplication } from '../../../../src';
+import { execution } from './app';
+
+async function main() {
+  const app: ArtusApplication = new ArtusApplication();
+  await app.load({
+    items: [
+      {
+        path: path.resolve(__dirname, './app'),
+        extname: '.ts',
+        filename: 'app.ts',
+        loader: 'extension',
+        source: 'app'
+      },
+      {
+        path: path.resolve(__dirname, './timerTrigger'),
+        extname: '.ts',
+        filename: 'timerTrigger.ts',
+        loader: 'module',
+        source: 'app'
+      }
+    ]
+  });
+  await app.run();
+
+  return app;
+};
+
+function getTaskExecution() {
+  return execution;
+}
+
+export {
+  main,
+  getTaskExecution
+};
diff --git a/test/loader.test.ts b/test/loader.test.ts
index 168dc71..ba1e260 100644
--- a/test/loader.test.ts
+++ b/test/loader.test.ts
@@ -2,13 +2,20 @@ import 'reflect-metadata';
 
 import { Container } from '@artus/injection';
 import assert from 'assert';
-import { LoaderFactory } from '../src';
+import { Application, ArtusInjectEnum, LifecycleManager, LoaderFactory } from '../src';
+import ConfigurationHandler from '../src/configuration';
 
 describe('test/loader.test.ts', () => {
   describe('module with ts', () => {
     it('should load module testServiceA.ts and testServiceB.ts', async () => {
       const container = new Container('testDefault');
       const loaderFactory = LoaderFactory.create(container);
+
+      // Mock for loader
+      const lifecycleManager = new LifecycleManager(null as unknown as Application, container);
+      container.set({ id: ArtusInjectEnum.LifecycleManager, value: lifecycleManager });
+      container.set({ type: ConfigurationHandler });
+
       const manifest = require('./fixtures/module-with-ts/src/index').default;
       await loaderFactory.loadManifest(manifest);
       assert((container.get('testServiceA') as any).testMethod() === 'Hello Artus');
@@ -18,6 +25,12 @@ describe('test/loader.test.ts', () => {
     it('should load module testServiceA.js and testServiceB.js', async () => {
       const container = new Container('testDefault');
       const loaderFactory = LoaderFactory.create(container);
+
+      // Mock for loader
+      const lifecycleManager = new LifecycleManager(null as unknown as Application, container);
+      container.set({ id: ArtusInjectEnum.LifecycleManager, value: lifecycleManager });
+      container.set({ type: ConfigurationHandler });
+
       const manifest = require('./fixtures/module-with-js/src/index');
       await loaderFactory.loadManifest(manifest);
       const appProxy = new Proxy({}, {
diff --git a/test/plugin.test.ts b/test/plugin.test.ts
index fcc2792..98f1899 100644
--- a/test/plugin.test.ts
+++ b/test/plugin.test.ts
@@ -11,14 +11,35 @@ describe('test/app.test.ts', () => {
         'plugin-a': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-a`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-a/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-b': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-b`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-b/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-c': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-c`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-c/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         }
       }
       const pluginList = await PluginFactory.createFromConfig(mockPluginConfig);
@@ -35,22 +56,57 @@ describe('test/app.test.ts', () => {
         'plugin-a': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-a`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-a/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-b': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-b`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-b/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-c': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-c`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-c/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-wrong-a': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-wrong-a`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-wrong-a/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-wrong-b': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-wrong-b`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-wrong-b/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         }
       }
       expect(async () => {
@@ -58,11 +114,18 @@ describe('test/app.test.ts', () => {
       }).rejects.toThrowError(new Error(`There is a cycle in the dependencies, wrong plugin is plugin-wrong-a,plugin-wrong-b.`));
     });
 
-    it('should throw if dependence missing', async () => {
+    it('should throw if dependencies missing', async () => {
       const mockPluginConfig = {
         'plugin-a': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-a`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-a/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         }
       }
       expect(async () => {
@@ -75,14 +138,35 @@ describe('test/app.test.ts', () => {
         'plugin-a': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-a`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-a/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-b': {
           enable: false,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-b`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-b/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         },
         'plugin-c': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-c`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-c/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         }
       }
       expect(async () => {
@@ -95,22 +179,27 @@ describe('test/app.test.ts', () => {
         'plugin-d': {
           enable: true,
           path: path.resolve(__dirname, `${pluginPrefix}/plugin-d`),
+          manifest: {
+            pluginMeta: {
+              path: path.resolve(__dirname, `${pluginPrefix}/plugin-d/meta.js`),
+              extname: '.js',
+              filename: 'meta.js',
+            }
+          }
         }
       }
-      let warnMessage = '';
 
       // mock warn
       const originWarn = console.warn;
-      console.warn = function (msg) {
-        warnMessage = msg;
-      }
+      const mockWarnFn = jest.fn();
+      console.warn = mockWarnFn;
       const pluginList = await PluginFactory.createFromConfig(mockPluginConfig);
       expect(pluginList.length).toEqual(1);
       pluginList.forEach(plugin => {
         expect(plugin).toBeInstanceOf(ArtusPlugin);
         expect(plugin.enable).toBeTruthy();
       });
-      expect(warnMessage === 'Plugin plugin-d need have optional dependence: plugin-e.').toBeTruthy();
+      expect(mockWarnFn).toBeCalledWith(`Plugin plugin-d need have optional dependence: plugin-e.`);
 
       // restore warn
       console.warn = originWarn;
diff --git a/test/scanner.test.ts b/test/scanner.test.ts
index 6ef85b1..8f6bbac 100644
--- a/test/scanner.test.ts
+++ b/test/scanner.test.ts
@@ -7,18 +7,17 @@ describe('test/scanner.test.ts', () => {
         const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'] });
         const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app-koa-with-ts'));
         expect(manifest).toBeDefined();
-        const appManifest = manifest.app;
-        expect(appManifest.items).toBeDefined();
-        expect(appManifest.packageJson).toBeDefined();
-        expect(appManifest.config!.length).toBe(1);
-        expect(appManifest.pluginConfig!.length).toBe(1);
-        expect(appManifest.pluginConfig!.length).toBe(1);
-        expect(appManifest.exception!.length).toBe(1);
-        expect(appManifest.extension!.length).toBe(1);
+        expect(manifest.items).toBeDefined();
+        expect(manifest.items.length).toBe(11);
 
-        expect(manifest.plugins).toBeDefined();
-        const pluginManifest = manifest.plugins['redis'];
-        expect(pluginManifest.pluginMeta).toBeDefined();
-        expect(pluginManifest.extension!.length).toBe(1);
-    })
+        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 === 'exception').length).toBe(1);
+        expect(manifest.items.filter(item => item.loader === 'extension').length).toBe(2);
+        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.source === 'redis').length).toBe(2);
+        expect(manifest.items.filter(item => item.source === 'app').length).toBe(9);
+    });
 });
\ No newline at end of file
diff --git a/test/trigger/event.test.ts b/test/trigger/event.test.ts
index aa96795..ba0a89f 100644
--- a/test/trigger/event.test.ts
+++ b/test/trigger/event.test.ts
@@ -6,7 +6,7 @@ describe('test/trigger/event.test.ts', () => {
     const {
       main,
       pub
-    } = await import('../fixtures/trigger/event/app');
+    } = await import('../fixtures/trigger/event/bootstrap');
     const app = await main();
     let e1Result, e2Result;
     pub('e1', {
diff --git a/test/trigger/http.test.ts b/test/trigger/http.test.ts
index e1e5101..d11d8ae 100644
--- a/test/trigger/http.test.ts
+++ b/test/trigger/http.test.ts
@@ -7,7 +7,7 @@ describe('test/trigger/http.test.ts', () => {
     const {
       main,
       isListening
-    } = await import('../fixtures/trigger/http/app');
+    } = await import('../fixtures/trigger/http/bootstrap');
     const app = await main();
     const testResponse = await axios.get('http://127.0.0.1:3001');
     await axios.get('http://127.0.0.1:3001');
diff --git a/test/trigger/timer.test.ts b/test/trigger/timer.test.ts
index e75cdf6..e85e3c0 100644
--- a/test/trigger/timer.test.ts
+++ b/test/trigger/timer.test.ts
@@ -6,7 +6,7 @@ describe('test/trigger/timer.test.ts', () => {
     const {
       main,
       getTaskExecution,
-    } = await import('../fixtures/trigger/timer/app');
+    } = await import('../fixtures/trigger/timer/bootstrap');
     const app = await main();
     await new Promise(resolve => setTimeout(resolve, 3000));
     const execution = getTaskExecution();