Repository: artusjs/core
Pull request #131 (2022-07-11 12:47:25)

refactor: app add loader hook

JerrysShan: 这个改动主要是为了暴露自定义 loader hook 的API ， 满足上层框架自定义 loader hook 的需求
codecov-commenter: # [Codecov](https://codecov.io/gh/artusjs/core/pull/131?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> :exclamation: No coverage uploaded for pull request base (`master@73e21be`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#section-missing-base-commit).
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff            @@
##             master     #131   +/-   ##
=========================================
  Coverage          ?   88.97%           
=========================================
  Files             ?       50           
  Lines             ?     1079           
  Branches          ?      172           
=========================================
  Hits              ?      960           
  Misses            ?      119           
  Partials          ?        0           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/artusjs/core/pull/131?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/artusjs/core/pull/131?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [73e21be...bdf6676](https://codecov.io/gh/artusjs/core/pull/131?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

hyj1991: `style` 大量更改最好单独拆 pr，不要合并到 feature 的 pr 里，看起来有点乱
hyj1991: `LoaderEventEmitter ` 自身的实现看起来并没有问题
JerrysShan: > `style` 大量更改最好单独拆 pr，不要合并到 feature 的 pr 里，看起来有点乱

我看下，开发完运行了下 `lint:fix` ，貌似就有 format code 的更改了

hyj1991: > > `style` 大量更改最好单独拆 pr，不要合并到 feature 的 pr 里，看起来有点乱
> 
> 我看下，开发完运行了下 `lint:fix` ，貌似就有 format code 的更改了

这个 pr 就不用改了，下次可以分开提下

Patch:
diff --git a/jest.config.js b/jest.config.js
index 890e120..43eb663 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,13 +1,15 @@
 /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
 module.exports = {
   transform: {
-      '^.+\\.jsx?$': 'babel-jest',
-      '^.+\\.tsx?$': 'ts-jest',
+    '^.+\\.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'
-  ]
+  ],
+  coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover', 'text-summary', 'cobertura'],
+  collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
 };
diff --git a/package.json b/package.json
index dbfa1a7..859c9eb 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
   ],
   "scripts": {
     "build": "tsc -p ./tsconfig.build.json",
-    "test": "jest --detectOpenHandles",
+    "test": "jest --coverage --detectOpenHandles",
     "ci": "npm run lint && npm run test",
     "lint:fix": "eslint . --ext .ts --fix",
     "lint": "eslint . --ext .ts"
diff --git a/src/application.ts b/src/application.ts
index d754328..6a49384 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -21,6 +21,7 @@ export class ArtusApplication implements Application {
     this.lifecycleManager = new LifecycleManager(this, this.container);
     this.loaderFactory = LoaderFactory.create(this.container);
 
+    this.addLoaderListener();
     this.loadDefaultClass();
 
     process.on('SIGINT', () => this.close(true));
@@ -105,4 +106,32 @@ export class ArtusApplication implements Application {
   createException(code: string): ArtusStdError {
     return this.exceptionHandler.create(code);
   }
+
+  protected addLoaderListener() {
+    this.loaderFactory
+      .addLoaderListener('config', {
+        before: () => this.lifecycleManager.emitHook('configWillLoad'),
+        after: () => {
+          this.container.set({
+            id: ArtusInjectEnum.Config,
+            value: this.configurationHandler.getAllConfig(),
+          });
+          this.lifecycleManager.emitHook('configDidLoad');
+        },
+      })
+      .addLoaderListener('framework-config', {
+        after: () =>
+          this.container.set({
+            id: ArtusInjectEnum.Frameworks,
+            value: this.configurationHandler.getFrameworkConfig(),
+          }),
+      })
+      .addLoaderListener('package-json', {
+        after: () =>
+          this.container.set({
+            id: ArtusInjectEnum.Packages,
+            value: this.configurationHandler.getPackages(),
+          }),
+      });
+  }
 }
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index 5c8eeea..6491f0d 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -6,9 +6,9 @@ import compatibleRequire from '../utils/compatible_require';
 import { DefineConfigHandle } from './decorator';
 
 export type ConfigObject = Record<string, any>;
-export type FrameworkObject = { path: string, env: string };
+export type FrameworkObject = { path: string; env: string };
 export type PackageObject = ConfigObject;
-export type FrameworkOptions = { env: string, unitName: string };
+export type FrameworkOptions = { env: string; unitName: string };
 
 @Injectable()
 export default class ConfigurationHandler {
@@ -31,6 +31,14 @@ export default class ConfigurationHandler {
     return mergeConfig(defaultConfig, envConfig);
   }
 
+  getAllConfig(): ConfigObject {
+    const defaultConfig = this.configStore.get(ARTUS_DEFAULT_CONFIG_ENV.DEFAULT) ?? {};
+    const keys = Array.from(this.configStore.keys()).filter(
+      key => key !== ARTUS_DEFAULT_CONFIG_ENV.DEFAULT,
+    );
+    return mergeConfig(defaultConfig, ...keys.map(key => this.configStore.get(key) ?? {}));
+  }
+
   setConfig(env: string, config: ConfigObject) {
     const storedConfig = this.configStore.get(env) ?? {};
     this.configStore.set(env, mergeConfig(storedConfig, config));
@@ -48,13 +56,15 @@ export default class ConfigurationHandler {
   getFrameworkConfig(
     env?: string,
     key = 'app',
-    frameworkMap = new Map<string, FrameworkObject>()): Map<string, FrameworkObject> {
+    frameworkMap = new Map<string, FrameworkObject>(),
+  ): Map<string, FrameworkObject> {
     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 FrameworkObject[];
-    const defaultConfig = list.filter(item => item.env === ARTUS_DEFAULT_CONFIG_ENV.DEFAULT)[0] ?? {};
+    const defaultConfig =
+      list.filter(item => item.env === ARTUS_DEFAULT_CONFIG_ENV.DEFAULT)[0] ?? {};
     const envConfig = list.filter(item => item.env === currentEnv)[0] ?? {};
     const config = mergeConfig(defaultConfig, envConfig) as unknown as FrameworkObject;
     frameworkMap.set(key, config);
diff --git a/src/constant.ts b/src/constant.ts
index 5ae338a..cd84884 100644
--- a/src/constant.ts
+++ b/src/constant.ts
@@ -33,7 +33,7 @@ export const HOOK_FILE_LOADER = 'appHook:fileLoader';
 export const HOOK_CONFIG_HANDLE = 'appHook:configHandle::';
 
 export const DEFAULT_EXCLUDES = [
-  'test',
+  'test/',
   'node_modules',
   '.*',
   'tsconfig*.json',
@@ -41,6 +41,7 @@ export const DEFAULT_EXCLUDES = [
   'jest.config.*',
   'meta.*',
   'LICENSE',
+  'pnpm-lock.yaml',
 ];
 
 export const FRAMEWORK_PATTERN = 'framework.*';
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index ab99c6d..83a7c39 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,14 +1,23 @@
 import * as path from 'path';
 import { Container } from '@artus/injection';
 import { ArtusInjectEnum, DEFAULT_LOADER, HOOK_FILE_LOADER, LOADER_NAME_META } from '../constant';
-import { Manifest, ManifestItem, LoaderConstructor, Loader, LoaderHookUnit, LoaderFindOptions, LoaderFindResult } from './types';
+import {
+  Manifest,
+  ManifestItem,
+  LoaderConstructor,
+  Loader,
+  LoaderFindOptions,
+  LoaderFindResult,
+} from './types';
 import ConfigurationHandler from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 import compatibleRequire from '../utils/compatible_require';
+import LoaderEventEmitter, { LoaderEventListener } from './loader_event';
 
 export class LoaderFactory {
   private container: Container;
   private static loaderClazzMap: Map<string, LoaderConstructor> = new Map();
+  private loaderEmitter: LoaderEventEmitter;
 
   static register(clazz: LoaderConstructor) {
     const loaderName = Reflect.getMetadata(LOADER_NAME_META, clazz);
@@ -17,6 +26,7 @@ export class LoaderFactory {
 
   constructor(container: Container) {
     this.container = container;
+    this.loaderEmitter = new LoaderEventEmitter();
   }
 
   static create(container: Container): LoaderFactory {
@@ -31,6 +41,16 @@ export class LoaderFactory {
     return this.container.get(ConfigurationHandler);
   }
 
+  addLoaderListener(eventName: string, listener: LoaderEventListener) {
+    this.loaderEmitter.addListener(eventName, listener);
+    return this;
+  }
+
+  removeLoaderListener(eventName: string, stage?: 'before' | 'after') {
+    this.loaderEmitter.removeListener(eventName, stage);
+    return this;
+  }
+
   getLoader(loaderName: string): Loader {
     const LoaderClazz = LoaderFactory.loaderClazzMap.get(loaderName);
     if (!LoaderClazz) {
@@ -40,56 +60,35 @@ export class LoaderFactory {
   }
 
   async loadManifest(manifest: Manifest, root?: string): Promise<void> {
-    await this.loadItemList(manifest.items, {
-      config: {
-        before: () => this.lifecycleManager.emitHook('configWillLoad'),
-        after: () => {
-          this.container.set({
-            id: ArtusInjectEnum.Config,
-            value: this.configurationHandler.getMergedConfig(),
-          });
-          this.lifecycleManager.emitHook('configDidLoad');
-        },
-      },
-      'framework-config': {
-        after: () => this.container.set({
-          id: ArtusInjectEnum.Frameworks,
-          value: this.configurationHandler.getFrameworkConfig(),
-        }),
-      },
-      'package-json': {
-        after: () => this.container.set({
-          id: ArtusInjectEnum.Packages,
-          value: this.configurationHandler.getPackages(),
-        }),
-      },
-    }, root);
+    await this.loadItemList(manifest.items, root);
   }
 
-  async loadItemList(itemList: ManifestItem[] = [], hookMap?: Record<string, LoaderHookUnit>, root?: string): Promise<void> {
+  async loadItemList(itemList: ManifestItem[] = [], root?: string): Promise<void> {
     let prevLoader = '';
     for (const item of itemList) {
       item.path = root ? path.join(root, item.path) : item.path;
       const curLoader = item.loader ?? DEFAULT_LOADER;
       if (item.loader !== prevLoader) {
         if (prevLoader) {
-          await hookMap?.[prevLoader]?.after?.();
+          await this.loaderEmitter.emitAfter(prevLoader);
         }
-        await hookMap?.[curLoader]?.before?.();
+        await this.loaderEmitter.emitBefore(curLoader);
         prevLoader = curLoader;
       }
-      await this.loadItem(item);
+      await this.loaderEmitter.emitBeforeEach(curLoader, item);
+      const result = await this.loadItem(item);
+      await this.loaderEmitter.emitAfterEach(curLoader, result);
     }
     if (prevLoader) {
-      await hookMap?.[prevLoader]?.after?.();
+      await this.loaderEmitter.emitAfter(prevLoader);
     }
   }
 
-  async loadItem(item: ManifestItem): Promise<void> {
+  loadItem(item: ManifestItem): Promise<any> {
     const loaderName = item.loader || DEFAULT_LOADER;
     const loader = this.getLoader(loaderName);
     loader.state = item._loaderState;
-    await loader.load(item);
+    return loader.load(item);
   }
 
   async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult> {
diff --git a/src/loader/impl/config.ts b/src/loader/impl/config.ts
index 2f18f0c..c64ccdc 100644
--- a/src/loader/impl/config.ts
+++ b/src/loader/impl/config.ts
@@ -50,6 +50,7 @@ class ConfigLoader implements Loader {
       };
     }
     this.configurationHandler.setConfig(env, configObj);
+    return configObj;
   }
 
   protected async getConfigFileMeta(item: ManifestItem): Promise<ConfigFileMeta> {
@@ -70,7 +71,7 @@ class ConfigLoader implements Loader {
   protected async loadConfigFile(item: ManifestItem): Promise<Record<string, any>> {
     const originConfigObj = await compatibleRequire(item.path);
     let configObj = originConfigObj;
-    if(typeof originConfigObj === 'function') {
+    if (typeof originConfigObj === 'function') {
       const app = this.container.get(ArtusInjectEnum.Application);
       configObj = originConfigObj(app);
     }
diff --git a/src/loader/impl/exception.ts b/src/loader/impl/exception.ts
index b10eadc..473c80d 100644
--- a/src/loader/impl/exception.ts
+++ b/src/loader/impl/exception.ts
@@ -22,12 +22,16 @@ class ExceptionLoader implements Loader {
   async load(item: ManifestItem) {
     const exceptionHandler = this.container.get(ExceptionHandler);
     try {
-      const codeMap: Record<string, ExceptionItem> = await loadMetaFile<Record<string, ExceptionItem>>(item.path);
+      const codeMap: Record<string, ExceptionItem> = await loadMetaFile<
+      Record<string, ExceptionItem>
+      >(item.path);
       for (const [errCode, exceptionItem] of Object.entries(codeMap)) {
         exceptionHandler.registerCode(errCode, exceptionItem);
       }
+      return codeMap;
     } catch (error) {
       console.warn(`[Artus-Exception] Parse CodeMap ${item.path} failed: ${error.message}`);
+      return void 0;
     }
   }
 }
diff --git a/src/loader/impl/framework_config.ts b/src/loader/impl/framework_config.ts
index 8d6bf30..2b82a8f 100644
--- a/src/loader/impl/framework_config.ts
+++ b/src/loader/impl/framework_config.ts
@@ -16,11 +16,12 @@ class FrameworkConfigLoader extends ConfigLoader implements Loader {
 
   async load(item: ManifestItem) {
     const { env } = await this.getConfigFileMeta(item);
-    const configObj = await this.loadConfigFile(item) as FrameworkObject;
+    const configObj = (await this.loadConfigFile(item)) as FrameworkObject;
     this.configurationHandler.addFramework(item.source || 'app', configObj, {
       env,
       unitName: item.unitName || '',
     });
+    return configObj;
   }
 }
 
diff --git a/src/loader/impl/lifecycle.ts b/src/loader/impl/lifecycle.ts
index 7565883..a02ec97 100644
--- a/src/loader/impl/lifecycle.ts
+++ b/src/loader/impl/lifecycle.ts
@@ -18,6 +18,7 @@ class LifecycleLoader implements Loader {
     const extClazz: Constructable<ApplicationLifecycle> = await compatibleRequire(item.path);
     const lifecycleManager: LifecycleManager = this.container.get(ArtusInjectEnum.LifecycleManager);
     lifecycleManager.registerHookUnit(extClazz);
+    return extClazz;
   }
 }
 
diff --git a/src/loader/impl/module.ts b/src/loader/impl/module.ts
index 6778e0a..4d6f617 100644
--- a/src/loader/impl/module.ts
+++ b/src/loader/impl/module.ts
@@ -24,9 +24,10 @@ class ModuleLoader implements Loader {
 
     const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz);
 
-    if(shouldOverwriteValue || !this.container.hasValue(opts)) {
+    if (shouldOverwriteValue || !this.container.hasValue(opts)) {
       this.container.set(opts);
     }
+    return moduleClazz;
   }
 }
 
diff --git a/src/loader/impl/package.ts b/src/loader/impl/package.ts
index b27fb77..4a27dfc 100644
--- a/src/loader/impl/package.ts
+++ b/src/loader/impl/package.ts
@@ -22,6 +22,7 @@ class PackageLoader implements Loader {
     const originConfigObj = await compatibleRequire(item.path);
     const configHandler = this.container.get(ConfigurationHandler);
     configHandler.addPackage(item.source || 'app', originConfigObj);
+    return originConfigObj;
   }
 }
 
diff --git a/src/loader/impl/plugin_config.ts b/src/loader/impl/plugin_config.ts
index 63464ef..032cac0 100644
--- a/src/loader/impl/plugin_config.ts
+++ b/src/loader/impl/plugin_config.ts
@@ -8,7 +8,6 @@ import ConfigLoader from './config';
 
 @DefineLoader('plugin-config')
 class PluginConfigLoader extends ConfigLoader implements Loader {
-
   static async is(opts: LoaderFindOptions): Promise<boolean> {
     if (this.isConfigDir(opts)) {
       return isMatch(opts.filename, PLUGIN_CONFIG_PATTERN);
@@ -24,7 +23,9 @@ class PluginConfigLoader extends ConfigLoader implements Loader {
       if (pluginConfigItem.package) {
         // convert package to path when load plugin config
         if (pluginConfigItem.path) {
-          throw new Error(`Plugin ${pluginName} config can't have both package and path at ${item.path}`);
+          throw new Error(
+            `Plugin ${pluginName} config can't have both package and path at ${item.path}`,
+          );
         }
         if (pluginConfigItem.enable) {
           pluginConfigItem.path = ArtusPlugin.getPath(pluginConfigItem.package);
@@ -36,6 +37,7 @@ class PluginConfigLoader extends ConfigLoader implements Loader {
     this.configurationHandler.setConfig(env, {
       plugin: configObj,
     });
+    return configObj;
   }
 }
 
diff --git a/src/loader/impl/plugin_meta.ts b/src/loader/impl/plugin_meta.ts
index 00bb3d4..9d25fd6 100644
--- a/src/loader/impl/plugin_meta.ts
+++ b/src/loader/impl/plugin_meta.ts
@@ -24,6 +24,7 @@ class PluginMetaLoader implements Loader {
       id: `pluginMeta_${pluginMeta.name}`,
       value: pluginMeta,
     });
+    return pluginMeta;
   }
 }
 
diff --git a/src/loader/loader_event.ts b/src/loader/loader_event.ts
new file mode 100644
index 0000000..9664b3c
--- /dev/null
+++ b/src/loader/loader_event.ts
@@ -0,0 +1,67 @@
+import { ManifestItem } from './types';
+
+export interface LoaderEventListener {
+  before?: CallableFunction;
+  after?: CallableFunction;
+
+  beforeEach?: (item: ManifestItem) => void;
+
+  afterEach?: (loadContent: any) => void;
+}
+
+export default class LoaderEventEmitter {
+  private listeners: Record<string, Record<'before' | 'after', CallableFunction[]>> = {};
+
+  addListener(eventName, listener: LoaderEventListener) {
+    if (!this.listeners[eventName]) {
+      this.listeners[eventName] = { before: [], after: [] };
+    }
+
+    if (listener.before) {
+      this.listeners[eventName].before.push(listener.before);
+    }
+
+    if (listener.after) {
+      this.listeners[eventName].after.push(listener.after);
+    }
+  }
+
+  removeListener(eventName: string, stage?: keyof LoaderEventListener) {
+    if (!this.listeners[eventName]) {
+      return;
+    }
+    if (stage) {
+      this.listeners[eventName][stage] = [];
+      return;
+    }
+
+    delete this.listeners[eventName];
+  }
+
+  async emitBefore(eventName, ...args) {
+    await this.emit(eventName, 'before', ...args);
+  }
+
+  async emitAfter(eventName, ...args) {
+    await this.emit(eventName, 'after', ...args);
+  }
+
+  async emitBeforeEach(eventName, ...args) {
+    await this.emit(eventName, 'beforeEach', ...args);
+  }
+
+  async emitAfterEach(eventName, ...args) {
+    await this.emit(eventName, 'afterEach', ...args);
+  }
+
+  async emit(eventName: string, stage: string, ...args) {
+    const stages = (this.listeners[eventName] ?? {})[stage];
+    if (!stages || stages.length === 0) {
+      return;
+    }
+
+    for (const listener of stages) {
+      await listener(...args);
+    }
+  }
+}
diff --git a/src/loader/types.ts b/src/loader/types.ts
index 0c0f5e2..4a6dd43 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -11,7 +11,7 @@ interface ManifestItem<LoaderState = unknown> extends Record<string, any> {
   filename: string;
   loader?: string;
   source?: string;
-  unitName?: string
+  unitName?: string;
   _loaderState?: LoaderState;
 }
 
@@ -28,18 +28,18 @@ interface LoaderFindResult {
 }
 
 interface LoaderHookUnit {
-  before?: Function,
-  after?: Function,
+  before?: Function;
+  after?: Function;
 }
 
 interface LoaderConstructor {
-  new(container: Container): Loader;
+  new (container: Container): Loader;
   is?(opts: LoaderFindOptions): Promise<boolean>;
   onFind?(opts: LoaderFindOptions): Promise<any>;
 }
 interface Loader {
   state?: any;
-  load(item: ManifestItem): Promise<void>;
+  load(item: ManifestItem): Promise<any>;
 }
 
 export {
@@ -51,4 +51,3 @@ export {
   LoaderFindOptions,
   LoaderFindResult,
 };
-
diff --git a/src/types.ts b/src/types.ts
index afd40ad..dfeffbc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,6 +14,7 @@ export interface ApplicationLifecycle {
 
 export interface ApplicationInitOptions {
   containerName?: string;
+  env: string;
 }
 
 export interface Application {
@@ -34,4 +35,3 @@ export interface TriggerType {
   initContext(...args): Promise<BaseContext>;
   startPipeline(...args): Promise<void>;
 }
-


Test patch:
diff --git a/test/loader.test.ts b/test/loader.test.ts
index c4c801b..30f13c7 100644
--- a/test/loader.test.ts
+++ b/test/loader.test.ts
@@ -26,11 +26,14 @@ describe('test/loader.test.ts', () => {
 
       const manifest = require('./fixtures/module_with_js/src/index');
       await loaderFactory.loadManifest(manifest);
-      const appProxy = new Proxy({}, {
-        get(_target, properName: string) {
-          return container.get(properName);
+      const appProxy = new Proxy(
+        {},
+        {
+          get(_target, properName: string) {
+            return container.get(properName);
+          },
         },
-      });
+      );
       assert((container.get('testServiceA') as any).testMethod(appProxy) === 'Hello Artus');
     });
   });
@@ -38,7 +41,9 @@ describe('test/loader.test.ts', () => {
   describe('module with custom loader', () => {
     it('should load module test.ts with custom loader', async () => {
       // SEEME: the import&register code need be replaced by scanner at production.
-      const { default: TestCustomLoader } = require('./fixtures/module_with_custom_loader/src/loader/test_custom_loader');
+      const {
+        default: TestCustomLoader,
+      } = require('./fixtures/module_with_custom_loader/src/loader/test_custom_loader');
       LoaderFactory.register(TestCustomLoader);
 
       const { default: manifest } = require('./fixtures/module_with_custom_loader/src/index');
@@ -66,4 +71,60 @@ describe('test/loader.test.ts', () => {
       expect(app.container.get(Custom).getName()).toBe('foo');
     });
   });
+
+  describe('loader event', () => {
+    it('should emit loader event', async () => {
+      const container = new Container('testDefault');
+      const loaderFactory = LoaderFactory.create(container);
+      const cb = jest.fn();
+      loaderFactory.addLoaderListener('module', {
+        before: () => {
+          cb();
+        },
+      });
+
+      const manifest = require('./fixtures/module_with_ts/src/index').default;
+      await loaderFactory.loadManifest(manifest);
+      expect(cb).toBeCalled();
+    });
+
+    it('should remove listener success', async () => {
+      const container = new Container('testDefault');
+      const loaderFactory = LoaderFactory.create(container);
+      const cb = jest.fn();
+      loaderFactory.addLoaderListener('module', {
+        before: () => {
+          cb();
+        },
+      });
+
+      loaderFactory.removeLoaderListener('module');
+
+      const manifest = require('./fixtures/module_with_ts/src/index').default;
+      await loaderFactory.loadManifest(manifest);
+      expect(cb).not.toBeCalled();
+    });
+
+    it('should remove listener success with stage', async () => {
+      const container = new Container('testDefault');
+      const loaderFactory = LoaderFactory.create(container);
+      const cb = jest.fn();
+      const afterCallback = jest.fn();
+      loaderFactory.addLoaderListener('module', {
+        before: () => {
+          cb();
+        },
+        after: () => {
+          afterCallback();
+        },
+      });
+
+      loaderFactory.removeLoaderListener('module', 'after');
+
+      const manifest = require('./fixtures/module_with_ts/src/index').default;
+      await loaderFactory.loadManifest(manifest);
+      expect(cb).toBeCalled();
+      expect(afterCallback).not.toBeCalled();
+    });
+  });
 });