Repository: artusjs/core
Pull request #180 (2022-08-23 12:50:15)

feat: support inject named export
此 PR 旨在支持具名导出，Scanner 中提供参数 `policy` 控制扫描期行为：
* `ScanPolicy.All`：默认值，会扫描全部的默认导出 & 具名导出
* `ScanPolicy.NamedExport`：只扫描具名导出，忽略默认导出
* `ScanPolicy.DefaultExport`：只扫描默认导出，忽略具名导出

当前实现中，横跨扫描期和启动期的状态由 `ManifestItem._loaderState` 保留字段处理，这个可以考虑转换为公有属性透出给用户处理两个阶段的状态保存。

注意可能的 break：如果想要完全兼容 master 的逻辑，默认值可以考虑更换为 `ScanPolicy.DefaultExport`，这一点需要确认下

close https://github.com/artusjs/core/issues/163

codecov-commenter: # [Codecov](https://codecov.io/gh/artusjs/core/pull/180?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#180](https://codecov.io/gh/artusjs/core/pull/180?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d31bccd) into [master](https://codecov.io/gh/artusjs/core/commit/f3e5a2fd1c2c1674d1722567a697ebe425438694?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (f3e5a2f) will **increase** coverage by `0.26%`.
> The diff coverage is `97.91%`.

```diff
@@            Coverage Diff             @@
##           master     #180      +/-   ##
==========================================
+ Coverage   89.61%   89.88%   +0.26%     
==========================================
  Files          51       51              
  Lines        1098     1117      +19     
  Branches      188      195       +7     
==========================================
+ Hits          984     1004      +20     
+ Misses        114      113       -1     
```


| [Impacted Files](https://codecov.io/gh/artusjs/core/pull/180?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/scanner/utils.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3NjYW5uZXIvdXRpbHMudHM=) | `94.82% <ø> (ø)` | |
| [src/loader/factory.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2xvYWRlci9mYWN0b3J5LnRz) | `92.39% <96.42%> (-0.11%)` | :arrow_down: |
| [src/constant.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvbnN0YW50LnRz) | `100.00% <100.00%> (ø)` | |
| [src/loader/impl/module.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2xvYWRlci9pbXBsL21vZHVsZS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/scanner/scan.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3NjYW5uZXIvc2Nhbi50cw==) | `95.07% <100.00%> (ø)` | |
| [src/utils/compatible\_require.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3V0aWxzL2NvbXBhdGlibGVfcmVxdWlyZS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/utils/is.ts](https://codecov.io/gh/artusjs/core/pull/180/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3V0aWxzL2lzLnRz) | `84.37% <0.00%> (+6.25%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/package.json b/package.json
index d5d39a4..034393d 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
   ],
   "scripts": {
     "build": "tsc -p ./tsconfig.build.json",
-    "test": "jest --coverage --detectOpenHandles",
+    "test": "jest --coverage --detectOpenHandles --testTimeout=15000",
     "ci": "npm run lint && npm run test",
     "lint:fix": "eslint . --ext .ts --fix",
     "lint": "eslint . --ext .ts"
diff --git a/src/constant.ts b/src/constant.ts
index 51be2b3..2b15f23 100644
--- a/src/constant.ts
+++ b/src/constant.ts
@@ -14,16 +14,22 @@ export enum ArtusInjectEnum {
   Trigger = 'artus#trigger',
 }
 
-export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';
-
-export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV';
-
 export enum ARTUS_DEFAULT_CONFIG_ENV {
   DEV = 'development',
   PROD = 'production',
   DEFAULT = 'default',
 }
 
+export enum ScanPolicy {
+  NamedExport = 'named_export',
+  DefaultExport = 'default_export',
+  All = "all",
+}
+
+export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';
+
+export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV';
+
 export const HOOK_NAME_META_PREFIX = 'hookName:';
 export const HOOK_FILE_LOADER = 'appHook:fileLoader';
 export const HOOK_CONFIG_HANDLE = 'appHook:configHandle::';
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index 995d217..ccb744e 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,6 +1,6 @@
 import * as path from 'path';
 import { isInjectable, Container } from '@artus/injection';
-import { ArtusInjectEnum, DEFAULT_LOADER, HOOK_FILE_LOADER, LOADER_NAME_META } from '../constant';
+import { ArtusInjectEnum, DEFAULT_LOADER, HOOK_FILE_LOADER, LOADER_NAME_META, ScanPolicy } from '../constant';
 import {
   Manifest,
   ManifestItem,
@@ -92,8 +92,9 @@ export class LoaderFactory {
     return loader.load(item);
   }
 
-  async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult|null> {
-    const loaderName = await this.findLoaderName(opts);
+  async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult | null> {
+    const { loader: loaderName, exportNames } = await this.findLoaderName(opts);
+
     if (!loaderName) {
       return null;
     }
@@ -104,6 +105,7 @@ export class LoaderFactory {
     }
     const result: LoaderFindResult = {
       loaderName,
+      loaderState: { exportNames },
     };
     if (loaderClazz.onFind) {
       result.loaderState = await loaderClazz.onFind(opts);
@@ -111,33 +113,55 @@ export class LoaderFactory {
     return result;
   }
 
-  async findLoaderName(opts: LoaderFindOptions): Promise<string|null> {
+  async findLoaderName(opts: LoaderFindOptions): Promise<{ loader: string | null, exportNames: string[] }> {
     for (const [loaderName, LoaderClazz] of LoaderFactory.loaderClazzMap.entries()) {
       if (await LoaderClazz.is?.(opts)) {
-        return loaderName;
+        return { loader: loaderName, exportNames: [] };
       }
     }
-    const { root, filename } = opts;
+    const { root, filename, policy = ScanPolicy.All } = opts;
 
     // require file for find loader
-    const targetClazz = await compatibleRequire(path.join(root, filename));
-    if (!isClass(targetClazz)) {
-      // The file is not export with default class
-      return null;
-    }
+    const allExport = await compatibleRequire(path.join(root, filename), true);
+    const exportNames: string[] = [];
+
+    let loaders = Object.entries(allExport)
+      .map(([name, targetClazz]) => {
+        if (!isClass(targetClazz)) {
+          // The file is not export with default class
+          return null;
+        }
 
-    // get loader from reflect metadata
-    const loaderMd = Reflect.getMetadata(HOOK_FILE_LOADER, targetClazz);
-    if (loaderMd?.loader) {
-      return loaderMd.loader;
-    }
+        if (policy === ScanPolicy.NamedExport && name === 'default') {
+          return null;
+        }
+
+        if (policy === ScanPolicy.DefaultExport && name !== 'default') {
+          return null;
+        }
+
+        // get loader from reflect metadata
+        const loaderMd = Reflect.getMetadata(HOOK_FILE_LOADER, targetClazz);
+        if (loaderMd?.loader) {
+          exportNames.push(name);
+          return loaderMd.loader;
+        }
+
+        // default loder with @Injectable
+        const injectableMd = isInjectable(targetClazz);
+        if (injectableMd) {
+          exportNames.push(name);
+          return DEFAULT_LOADER;
+        }
+      })
+      .filter(v => v);
+
+    loaders = Array.from(new Set(loaders));
 
-    // default loder with @Injectable
-    const injectableMd = isInjectable(targetClazz);
-    if (injectableMd) {
-      return DEFAULT_LOADER;
+    if (loaders.length > 1) {
+      throw new Error(`Not support multiple loaders for ${path.join(root, filename)}`);
     }
 
-    return null;
+    return { loader: loaders[0] ?? null, exportNames };
   }
 }
diff --git a/src/loader/impl/module.ts b/src/loader/impl/module.ts
index f61ba32..ba2e989 100644
--- a/src/loader/impl/module.ts
+++ b/src/loader/impl/module.ts
@@ -1,4 +1,4 @@
-import { Container, InjectableDefinition, ScopeEnum } from '@artus/injection';
+import { Constructable, Container, InjectableDefinition, ScopeEnum } from '@artus/injection';
 import { DefineLoader } from '../decorator';
 import { ManifestItem, Loader } from '../types';
 import compatibleRequire from '../../utils/compatible_require';
@@ -12,23 +12,34 @@ class ModuleLoader implements Loader {
     this.container = container;
   }
 
-  async load(item: ManifestItem) {
-    const moduleClazz = await compatibleRequire(item.path);
-    const opts: Partial<InjectableDefinition> = {
-      path: item.path,
-      type: moduleClazz,
-      scope: ScopeEnum.EXECUTION, // The class used with @artus/core will have default scope EXECUTION, can be overwritten by Injectable decorator
-    };
-    if (item.id) {
-      opts.id = item.id;
-    }
+  async load(item: ManifestItem): Promise<Constructable[]> {
+    const origin = await compatibleRequire(item.path, true);
+    item._loaderState = Object.assign({ exportNames: ['default'] }, item._loaderState);
+    const { _loaderState: state } = item as { _loaderState: { exportNames: string[] } };
+
+    const modules: Constructable[] = [];
+
+    for (const name of state.exportNames) {
+      const moduleClazz = origin[name];
+      const opts: Partial<InjectableDefinition> = {
+        path: item.path,
+        type: moduleClazz,
+        scope: ScopeEnum.EXECUTION, // The class used with @artus/core will have default scope EXECUTION, can be overwritten by Injectable decorator
+      };
+      if (item.id) {
+        opts.id = item.id;
+      }
 
-    const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz);
+      const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz);
 
-    if (shouldOverwriteValue || !this.container.hasValue(opts)) {
-      this.container.set(opts);
+      if (shouldOverwriteValue || !this.container.hasValue(opts)) {
+        this.container.set(opts);
+      }
+
+      modules.push(moduleClazz);
     }
-    return moduleClazz;
+
+    return modules;
   }
 }
 
diff --git a/src/loader/types.ts b/src/loader/types.ts
index 4a6dd43..5e1ddc2 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -1,4 +1,5 @@
 import { Container } from '@artus/injection';
+import { ScanPolicy } from '../constant';
 
 interface Manifest {
   items: ManifestItem[];
@@ -20,6 +21,7 @@ interface LoaderFindOptions {
   root: string;
   baseDir: string;
   configDir: string;
+  policy?: ScanPolicy;
 }
 
 interface LoaderFindResult {
diff --git a/src/scanner/scan.ts b/src/scanner/scan.ts
index 51567f0..a6af857 100644
--- a/src/scanner/scan.ts
+++ b/src/scanner/scan.ts
@@ -10,6 +10,7 @@ import {
   DEFAULT_EXCLUDES,
   DEFAULT_LOADER_LIST_WITH_ORDER,
   LOADER_NAME_META,
+  ScanPolicy,
 } from '../constant';
 import { LoaderFactory, Manifest, ManifestItem } from '../loader';
 import { ScannerOptions, WalkOptions } from './types';
@@ -37,6 +38,7 @@ export class Scanner {
       useRelativePath: true,
       configDir: DEFAULT_CONFIG_DIR,
       loaderListGenerator: (defaultLoaderList: string[]) => defaultLoaderList,
+      policy: ScanPolicy.All,
       ...options,
       exclude: DEFAULT_EXCLUDES.concat(options.exclude ?? []),
       extensions: [...new Set(this.moduleExtensions.concat(options.extensions ?? []))],
@@ -169,11 +171,12 @@ export class Scanner {
       if (ScanUtils.isExclude(filename, extname, this.options.exclude, this.options.extensions)) {
         return null;
       }
-      let loader = await loaderFactory.findLoaderName({
+      let { loader } = await loaderFactory.findLoaderName({
         filename,
         baseDir,
         root,
         configDir,
+        policy: this.options.policy,
       });
       if (loader === 'framework-config') {
         // SEEME: framework-config is a special loader, cannot be used when scan, need refactor later
@@ -242,6 +245,7 @@ export class Scanner {
       extensions: this.options.extensions,
       exclude: this.options.exclude,
       configDir: this.options.configDir,
+      policy: this.options.policy,
     };
 
     if (source === 'plugin') {
diff --git a/src/scanner/types.ts b/src/scanner/types.ts
index dcc0dbf..6cc1792 100644
--- a/src/scanner/types.ts
+++ b/src/scanner/types.ts
@@ -2,6 +2,7 @@ import { BaseLoader, ManifestItem } from "../loader";
 import { FrameworkConfig } from "../framework";
 import { PluginConfigItem } from "../plugin/types";
 import { Application } from "../types";
+import { ScanPolicy } from "../constant";
 
 export interface ScannerOptions {
   appName: string;
@@ -10,6 +11,7 @@ export interface ScannerOptions {
   useRelativePath: boolean;
   exclude: string[];
   configDir: string;
+  policy: ScanPolicy;
   envs?: string[];
   framework?: FrameworkConfig;
   plugin?: Record<string, Partial<PluginConfigItem>>;
@@ -21,6 +23,7 @@ export interface WalkOptions {
   source: string;
   baseDir: string;
   configDir: string;
+  policy: ScanPolicy;
   extensions: string[];
   exclude: string[];
   itemMap: Map<string, ManifestItem[]>;
diff --git a/src/scanner/utils.ts b/src/scanner/utils.ts
index e957c53..bd97463 100644
--- a/src/scanner/utils.ts
+++ b/src/scanner/utils.ts
@@ -57,6 +57,7 @@ export class ScanUtils {
           root,
           baseDir,
           configDir,
+          policy: options.policy,
         });
         if (!loaderFindResult) {
           continue;
diff --git a/src/utils/compatible_require.ts b/src/utils/compatible_require.ts
index ec72f2e..87960f3 100644
--- a/src/utils/compatible_require.ts
+++ b/src/utils/compatible_require.ts
@@ -4,10 +4,10 @@ import assert from 'assert';
  * compatible esModule require
  * @param path
  */
-export default async function compatibleRequire(path: string): Promise<any> {
+export default async function compatibleRequire(path: string, origin = false): Promise<any> {
   const requiredModule = await import(path);
 
   assert(requiredModule, `module '${path}' exports is undefined`);
 
-  return requiredModule.default || requiredModule;
+  return origin ? requiredModule : (requiredModule.default || requiredModule);
 }
\ No newline at end of file


Test patch:
diff --git a/test/fixtures/app_koa_with_ts/src/koa_app.ts b/test/fixtures/app_koa_with_ts/src/koa_app.ts
index 1640fdc..5ddbf99 100644
--- a/test/fixtures/app_koa_with_ts/src/koa_app.ts
+++ b/test/fixtures/app_koa_with_ts/src/koa_app.ts
@@ -1,5 +1,7 @@
-import { Injectable } from '@artus/injection';
+import { Injectable, ScopeEnum } from '@artus/injection';
 import Koa from 'koa';
 
-@Injectable()
-export default class KoaApplication extends Koa {}
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
+export default class KoaApplication extends Koa { }
diff --git a/test/fixtures/app_with_lifecycle/lifecyclelist.ts b/test/fixtures/app_with_lifecycle/lifecyclelist.ts
index 4eb2e18..7647701 100644
--- a/test/fixtures/app_with_lifecycle/lifecyclelist.ts
+++ b/test/fixtures/app_with_lifecycle/lifecyclelist.ts
@@ -1,6 +1,8 @@
-import { Injectable } from '../../../src';
+import { Injectable, ScopeEnum } from '../../../src';
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class LifecycleList {
   lifecycleList: string[] = [];
 
diff --git a/test/fixtures/application_specific/src/index.ts b/test/fixtures/application_specific/src/index.ts
index 49b0aea..57b6992 100644
--- a/test/fixtures/application_specific/src/index.ts
+++ b/test/fixtures/application_specific/src/index.ts
@@ -1,9 +1,11 @@
 import path from 'path';
 import { Manifest, ArtusApplication, ArtusInjectEnum } from "../../../../src";
 import { AbstractBar } from '../../frameworks/bar/src';
-import { Inject, Injectable } from "@artus/injection";
+import { Inject, Injectable, ScopeEnum } from "@artus/injection";
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class MyArtusApplication {
   @Inject('ABSTRACT_BAR')
   private bar: AbstractBar;
diff --git a/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_ob/src/client.ts b/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_ob/src/client.ts
index bddcea5..cd43ce4 100644
--- a/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_ob/src/client.ts
+++ b/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_ob/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_MYSQL',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_rds/src/client.ts b/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_rds/src/client.ts
index bddcea5..cd43ce4 100644
--- a/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_rds/src/client.ts
+++ b/test/fixtures/application_specific/src/plugins/artus_plugin_mysql_rds/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_MYSQL',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/application_specific/src/plugins/artus_plugin_redis/src/client.ts b/test/fixtures/application_specific/src/plugins/artus_plugin_redis/src/client.ts
index 9d73ff5..6d4a108 100644
--- a/test/fixtures/application_specific/src/plugins/artus_plugin_redis/src/client.ts
+++ b/test/fixtures/application_specific/src/plugins/artus_plugin_redis/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface RedisConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface RedisConfig {
 
 @Injectable({
   id: 'ARTUS_REDIS',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/artus_application/src/index.ts b/test/fixtures/artus_application/src/index.ts
index 49b0aea..57b6992 100644
--- a/test/fixtures/artus_application/src/index.ts
+++ b/test/fixtures/artus_application/src/index.ts
@@ -1,9 +1,11 @@
 import path from 'path';
 import { Manifest, ArtusApplication, ArtusInjectEnum } from "../../../../src";
 import { AbstractBar } from '../../frameworks/bar/src';
-import { Inject, Injectable } from "@artus/injection";
+import { Inject, Injectable, ScopeEnum } from "@artus/injection";
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class MyArtusApplication {
   @Inject('ABSTRACT_BAR')
   private bar: AbstractBar;
diff --git a/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_ob/src/client.ts b/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_ob/src/client.ts
index bddcea5..cd43ce4 100644
--- a/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_ob/src/client.ts
+++ b/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_ob/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_MYSQL',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_rds/src/client.ts b/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_rds/src/client.ts
index bddcea5..cd43ce4 100644
--- a/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_rds/src/client.ts
+++ b/test/fixtures/artus_application/src/plugins/artus_plugin_mysql_rds/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_MYSQL',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/artus_application/src/plugins/artus_plugin_redis/src/client.ts b/test/fixtures/artus_application/src/plugins/artus_plugin_redis/src/client.ts
index 9d73ff5..b381d71 100644
--- a/test/fixtures/artus_application/src/plugins/artus_plugin_redis/src/client.ts
+++ b/test/fixtures/artus_application/src/plugins/artus_plugin_redis/src/client.ts
@@ -1,11 +1,12 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface RedisConfig {
-  clientName: string
+  clientName: string,
 }
 
 @Injectable({
   id: 'ARTUS_REDIS',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/custom_instance/custom.ts b/test/fixtures/custom_instance/custom.ts
index 03cee03..ae616a6 100644
--- a/test/fixtures/custom_instance/custom.ts
+++ b/test/fixtures/custom_instance/custom.ts
@@ -1,6 +1,8 @@
-import { Injectable } from "../../../src/index";
+import { Injectable, ScopeEnum } from "../../../src/index";
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class Custom {
   private name: string;
 
diff --git a/test/fixtures/frameworks/bar/src/index.ts b/test/fixtures/frameworks/bar/src/index.ts
index 04d6024..0e5b9e2 100644
--- a/test/fixtures/frameworks/bar/src/index.ts
+++ b/test/fixtures/frameworks/bar/src/index.ts
@@ -1,10 +1,13 @@
-import { Inject, Injectable } from "@artus/injection";
+import { Inject, Injectable, ScopeEnum } from "@artus/injection";
 import { AbstractFoo } from "../../abstract/foo";
 
 export interface AbstractBar extends AbstractFoo { }
 
-@Injectable({ id: 'ABSTRACT_BAR' })
-export default class FrameworkBar implements AbstractBar {
+@Injectable({
+  id: 'ABSTRACT_BAR',
+  scope: ScopeEnum.SINGLETON,
+})
+export class FrameworkBar implements AbstractBar {
   @Inject('ABSTRACT_FOO')
   private foo: AbstractFoo;
 
diff --git a/test/fixtures/frameworks/layer/foo/foo1/src/index.ts b/test/fixtures/frameworks/layer/foo/foo1/src/index.ts
index 827abb3..b620bfc 100644
--- a/test/fixtures/frameworks/layer/foo/foo1/src/index.ts
+++ b/test/fixtures/frameworks/layer/foo/foo1/src/index.ts
@@ -1,9 +1,12 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 import { ArtusApplication } from '../../../../../../../src';
 import { server } from './lifecycle';
 
-@Injectable({ id: 'ABSTRACT_FOO' })
-export default class FrameworkFoo extends ArtusApplication {
+@Injectable({
+  id: 'ABSTRACT_FOO',
+  scope: ScopeEnum.SINGLETON,
+})
+export class FrameworkFoo extends ArtusApplication {
   isListening(): boolean {
     return server?.listening;
   }
diff --git a/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_hbase/src/client.ts b/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_hbase/src/client.ts
index 96ec9f5..34f7818 100644
--- a/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_hbase/src/client.ts
+++ b/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_hbase/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_HBASE',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_mysql_rds/src/client.ts b/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_mysql_rds/src/client.ts
index bddcea5..cd43ce4 100644
--- a/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_mysql_rds/src/client.ts
+++ b/test/fixtures/frameworks/layer/foo/foo2/node_modules/artus_plugin_mysql_rds/src/client.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 
 export interface MysqlConfig {
   clientName: string
@@ -6,6 +6,7 @@ export interface MysqlConfig {
 
 @Injectable({
   id: 'ARTUS_MYSQL',
+  scope: ScopeEnum.SINGLETON,
 })
 export default class Client {
   private clientName = '';
diff --git a/test/fixtures/frameworks/layer/foo/foo2/src/index.ts b/test/fixtures/frameworks/layer/foo/foo2/src/index.ts
index 827abb3..b620bfc 100644
--- a/test/fixtures/frameworks/layer/foo/foo2/src/index.ts
+++ b/test/fixtures/frameworks/layer/foo/foo2/src/index.ts
@@ -1,9 +1,12 @@
-import { Injectable } from "@artus/injection";
+import { Injectable, ScopeEnum } from "@artus/injection";
 import { ArtusApplication } from '../../../../../../../src';
 import { server } from './lifecycle';
 
-@Injectable({ id: 'ABSTRACT_FOO' })
-export default class FrameworkFoo extends ArtusApplication {
+@Injectable({
+  id: 'ABSTRACT_FOO',
+  scope: ScopeEnum.SINGLETON,
+})
+export class FrameworkFoo extends ArtusApplication {
   isListening(): boolean {
     return server?.listening;
   }
diff --git a/test/fixtures/logger/src/test_clazz.ts b/test/fixtures/logger/src/test_clazz.ts
index 81b05fb..e96e594 100644
--- a/test/fixtures/logger/src/test_clazz.ts
+++ b/test/fixtures/logger/src/test_clazz.ts
@@ -1,12 +1,14 @@
-import { Inject, Injectable } from '@artus/injection';
+import { Inject, Injectable, ScopeEnum } from '@artus/injection';
 import { ArtusLogger, LoggerLevel } from '../../../../src/logger';
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class TestLoggerClazz {
   @Inject()
   private logger!: ArtusLogger;
 
-  public testLog(level: LoggerLevel, message: string|Error, ...splat: any[]) {
+  public testLog(level: LoggerLevel, message: string | Error, ...splat: any[]) {
     this.logger.log({
       level,
       message,
@@ -30,7 +32,7 @@ export default class TestLoggerClazz {
     this.logger.warn(message, ...args);
   }
 
-  public testError(message: string|Error, ...args: any[]) {
+  public testError(message: string | Error, ...args: any[]) {
     this.logger.error(message, ...args);
   }
 }
diff --git a/test/fixtures/logger/src/test_custom_clazz.ts b/test/fixtures/logger/src/test_custom_clazz.ts
index 3a72218..87b4312 100644
--- a/test/fixtures/logger/src/test_custom_clazz.ts
+++ b/test/fixtures/logger/src/test_custom_clazz.ts
@@ -1,7 +1,9 @@
-import { Inject, Injectable } from '@artus/injection';
+import { Inject, Injectable, ScopeEnum } from '@artus/injection';
 import CustomLogger from './custom_logger';
 
-@Injectable()
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
 export default class TestCustomLoggerClazz {
   @Inject()
   private logger!: CustomLogger;
@@ -10,7 +12,7 @@ export default class TestCustomLoggerClazz {
     this.logger.info(message, ...args);
   }
 
-  public testError(message: string|Error, ...args: any[]) {
+  public testError(message: string | Error, ...args: any[]) {
     this.logger.error(message, ...args);
   }
 }
diff --git a/test/fixtures/named_export/package.json b/test/fixtures/named_export/package.json
new file mode 100644
index 0000000..17fd808
--- /dev/null
+++ b/test/fixtures/named_export/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "named-export"
+}
\ No newline at end of file
diff --git a/test/fixtures/named_export/src/config/config.default.ts b/test/fixtures/named_export/src/config/config.default.ts
new file mode 100644
index 0000000..a89c969
--- /dev/null
+++ b/test/fixtures/named_export/src/config/config.default.ts
@@ -0,0 +1,3 @@
+export default {
+  key: 'random',
+};
\ No newline at end of file
diff --git a/test/fixtures/named_export/src/index.ts b/test/fixtures/named_export/src/index.ts
new file mode 100644
index 0000000..9d814eb
--- /dev/null
+++ b/test/fixtures/named_export/src/index.ts
@@ -0,0 +1,5 @@
+export * from './mysql';
+
+import Redis from './redis';
+
+export { Redis };
\ No newline at end of file
diff --git a/test/fixtures/named_export/src/mysql.ts b/test/fixtures/named_export/src/mysql.ts
new file mode 100644
index 0000000..821a2a8
--- /dev/null
+++ b/test/fixtures/named_export/src/mysql.ts
@@ -0,0 +1,25 @@
+import { Injectable, ScopeEnum } from "../../../../src";
+
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
+export class Mysql {
+  private name = 'mysql';
+
+  getName() {
+    return this.name;
+  }
+}
+
+export const number = 1;
+
+export const object = { a: 1 };
+
+
+export class Mysql2 {
+  private name = 'mysql2';
+
+  getName() {
+    return this.name;
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/named_export/src/redis.ts b/test/fixtures/named_export/src/redis.ts
new file mode 100644
index 0000000..3e0760c
--- /dev/null
+++ b/test/fixtures/named_export/src/redis.ts
@@ -0,0 +1,23 @@
+import { Injectable, ScopeEnum } from "../../../../src";
+
+@Injectable({
+  scope: ScopeEnum.SINGLETON,
+})
+export default class Redis {
+  private name = 'redis';
+
+  getName() {
+    return this.name;
+  }
+}
+
+@Injectable()
+export class Redis2 {
+  private name = 'redis2';
+
+  getName() {
+    return this.name;
+  }
+}
+
+export { Redis };
\ No newline at end of file
diff --git a/test/loader.test.ts b/test/loader.test.ts
index db77cdd..040ded2 100644
--- a/test/loader.test.ts
+++ b/test/loader.test.ts
@@ -51,7 +51,7 @@ describe('test/loader.test.ts', () => {
       const container = new Container('testDefault');
       const loaderFactory = LoaderFactory.create(container);
 
-      const loaderName = await loaderFactory.findLoaderName({
+      const { loader: loaderName } = await loaderFactory.findLoaderName({
         filename: 'test_clazz.ts',
         root: path.resolve(__dirname, './fixtures/module_with_custom_loader/src'),
         baseDir: path.resolve(__dirname, './fixtures/module_with_custom_loader/src'),
diff --git a/test/scanner.test.ts b/test/scanner.test.ts
index 96f80da..5204fdb 100644
--- a/test/scanner.test.ts
+++ b/test/scanner.test.ts
@@ -1,6 +1,7 @@
+import 'reflect-metadata';
 import { Scanner } from '../src/scanner';
 import path from 'path';
-import { LoaderFactory } from '../src';
+import { ScanPolicy , LoaderFactory } from '../src';
 
 
 describe('test/scanner.test.ts', () => {
@@ -58,4 +59,28 @@ describe('test/scanner.test.ts', () => {
     expect(manifest.items.length).toBe(1);
     expect(manifest.items[0]?.loader).toBe('test-custom-loader');
   });
+
+  it('should scan application with all injectable class', async () => {
+    const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'] });
+    const scanResults = await scanner.scan(path.resolve(__dirname, './fixtures/named_export'));
+    const { default: manifest } = scanResults;
+    expect(manifest.items).toBeDefined();
+    expect(manifest.items.length).toBe(5);
+  });
+
+  it('should scan application with named export class', async () => {
+    const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'], policy: ScanPolicy.NamedExport });
+    const scanResults = await scanner.scan(path.resolve(__dirname, './fixtures/named_export'));
+    const { default: manifest } = scanResults;
+    expect(manifest.items).toBeDefined();
+    expect(manifest.items.length).toBe(5);
+  });
+
+  it('should scan application with default export class', async () => {
+    const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'], policy: ScanPolicy.DefaultExport });
+    const scanResults = await scanner.scan(path.resolve(__dirname, './fixtures/named_export'));
+    const { default: manifest } = scanResults;
+    expect(manifest.items).toBeDefined();
+    expect(manifest.items.length).toBe(3);
+  });
 });