Repository: artusjs/core (ts)

Issue:
[RFC] Loader 增强
## 背景

Scanner 阶段扫描文件根据自定义 Loader + 内置 Loader 的组合判断应用所有的文件分别应该被哪种 Loader 加载，其中自定义 Loader 尚存在一些使用问题：
* 需要手动 `import`，对于自定义 Loader 分散在多层上层框架封装中的场景下对应的 Scanner 使用不友好

因此需要内置在扫描期实现一个自动加载自定义 Loader 的功能，并且在扫描其余应用文件之前执行。

另外目前的 `DefineLoader` 实现的 `is` 方法仅对符合条件的扫描文件返回了本 Loader 名称，稍显简单，考虑在 `Manifest` 属性中添加一个自定义 Slot 对象：`Record<string, any>`，用来存储自定义扫描期处理的元信息给 `load` 逻辑消费，进一步减少可能存在的启动期的计算消耗。


## 实现

因此初步考虑 Loader 的增强如下能力：
* Scanner 扫描提供前置方法，内置对自定义 Loader 的公共处理
* Loader 本身增加一个 `handle` 方法，开放传入当前的 `scanner` 实例增强自定义 Loader 的能力

Pull request description:
feat(loader): is 局部重构为 static & 增加 State 支持
resolve #66 #87 

Patch:
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index c0edc7b..f7bb047 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,7 +1,7 @@
 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, LoaderHookUnit, LoaderCheckOptions } from './types';
+import { Manifest, ManifestItem, LoaderConstructor, Loader, LoaderHookUnit, LoaderFindOptions, LoaderFindResult } from './types';
 import ConfigurationHandler from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 import compatibleRequire from '../utils/compatible_require';
@@ -31,6 +31,14 @@ export class LoaderFactory {
     return this.container.get(ConfigurationHandler);
   }
 
+  getLoader(loaderName: string): Loader {
+    const LoaderClazz = LoaderFactory.loaderClazzMap.get(loaderName);
+    if (!LoaderClazz) {
+      throw new Error(`Cannot find loader '${loaderName}'`);
+    }
+    return new LoaderClazz(this.container);
+  }
+
   async loadManifest(manifest: Manifest, root?: string): Promise<void> {
     await this.loadItemList(manifest.items, {
       config: {
@@ -79,18 +87,29 @@ export class LoaderFactory {
 
   async loadItem(item: ManifestItem): Promise<void> {
     const loaderName = item.loader || DEFAULT_LOADER;
-    const LoaderClazz = LoaderFactory.loaderClazzMap.get(loaderName);
-    if (!LoaderClazz) {
+    const loader = this.getLoader(loaderName);
+    loader.state = item._loaderState;
+    await loader.load(item);
+  }
+
+  async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult> {
+    const loaderName = await this.findLoaderName(opts);
+    const loaderClazz = LoaderFactory.loaderClazzMap.get(loaderName);
+    if (!loaderClazz) {
       throw new Error(`Cannot find loader '${loaderName}'`);
     }
-    const loader = new LoaderClazz(this.container);
-    await loader.load(item);
+    const result: LoaderFindResult = {
+      loaderName,
+    };
+    if (loaderClazz.onFind) {
+      result.loaderState = await loaderClazz.onFind(opts);
+    }
+    return result;
   }
 
-  async getLoaderName(opts: LoaderCheckOptions): Promise<string> {
+  async findLoaderName(opts: LoaderFindOptions): Promise<string> {
     for (const [loaderName, LoaderClazz] of LoaderFactory.loaderClazzMap.entries()) {
-      const loader = new LoaderClazz(this.container);
-      if (await loader.is?.(opts)) {
+      if (await LoaderClazz.is?.(opts)) {
         return loaderName;
       }
     }
diff --git a/src/loader/impl/config.ts b/src/loader/impl/config.ts
index 877df9c..f1209c3 100644
--- a/src/loader/impl/config.ts
+++ b/src/loader/impl/config.ts
@@ -3,7 +3,7 @@ import { Container } from '@artus/injection';
 import ConfigurationHandler from '../../configuration';
 import { ArtusInjectEnum, ARTUS_DEFAULT_CONFIG_ENV, CONFIG_PATTERN } from '../../constant';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader, LoaderCheckOptions } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import compatibleRequire from '../../utils/compatible_require';
 import { isMatch } from '../../utils';
 
@@ -15,14 +15,14 @@ class ConfigLoader implements Loader {
     this.container = container;
   }
 
-  async is(opts: LoaderCheckOptions): Promise<boolean> {
+  static async is(opts: LoaderFindOptions): Promise<boolean> {
     if (this.isConfigDir(opts)) {
       return isMatch(opts.filename, CONFIG_PATTERN);
     }
     return false;
   }
 
-  protected isConfigDir(opts: LoaderCheckOptions): boolean {
+  protected static isConfigDir(opts: LoaderFindOptions): boolean {
       const { configDir, baseDir, root } = opts;
       return path.join(baseDir, configDir) === root;
   }
diff --git a/src/loader/impl/exception.ts b/src/loader/impl/exception.ts
index 78d1eb8..cf399c9 100644
--- a/src/loader/impl/exception.ts
+++ b/src/loader/impl/exception.ts
@@ -1,6 +1,6 @@
 import { Container } from '@artus/injection';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader, LoaderCheckOptions } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import { ExceptionItem } from '../../exception/types';
 import { ExceptionHandler } from '../../exception';
 import { loadMetaFile } from '../../utils/load_meta_file';
@@ -15,7 +15,7 @@ class ExceptionLoader implements Loader {
     this.container = container;
   }
 
-  async is(opts: LoaderCheckOptions) {
+  static async is(opts: LoaderFindOptions) {
     return isMatch(opts.filename, EXCEPTION_FILE);
   }
 
diff --git a/src/loader/impl/framework_config.ts b/src/loader/impl/framework_config.ts
index e814a40..01ff60b 100644
--- a/src/loader/impl/framework_config.ts
+++ b/src/loader/impl/framework_config.ts
@@ -1,6 +1,6 @@
 import ConfigurationHandler from '../../configuration';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader, LoaderCheckOptions } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import compatibleRequire from '../../utils/compatible_require';
 import { ArtusInjectEnum, ARTUS_DEFAULT_CONFIG_ENV, FRAMEWORK_PATTERN } from '../../constant';
 import ConfigLoader from './config';
@@ -12,7 +12,7 @@ class FrameworkConfigLoader extends ConfigLoader implements Loader {
     super(container);
   }
 
-  async is(opts: LoaderCheckOptions): Promise<boolean> {
+  static async is(opts: LoaderFindOptions): Promise<boolean> {
     if (this.isConfigDir(opts)) {
       return isMatch(opts.filename, FRAMEWORK_PATTERN);
     }
diff --git a/src/loader/impl/package.ts b/src/loader/impl/package.ts
index ce9a354..b27fb77 100644
--- a/src/loader/impl/package.ts
+++ b/src/loader/impl/package.ts
@@ -1,7 +1,7 @@
 import { Container } from '@artus/injection';
 import ConfigurationHandler from '../../configuration';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader, LoaderCheckOptions } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import compatibleRequire from '../../utils/compatible_require';
 import { PACKAGE_JSON } from '../../constant';
 import { isMatch } from '../../utils';
@@ -14,7 +14,7 @@ class PackageLoader implements Loader {
     this.container = container;
   }
 
-  async is(opts: LoaderCheckOptions) {
+  static async is(opts: LoaderFindOptions) {
     return isMatch(opts.filename, PACKAGE_JSON);
   }
 
diff --git a/src/loader/impl/plugin_config.ts b/src/loader/impl/plugin_config.ts
index 0f0b7c2..8a727ea 100644
--- a/src/loader/impl/plugin_config.ts
+++ b/src/loader/impl/plugin_config.ts
@@ -1,13 +1,13 @@
 import { PLUGIN_CONFIG_PATTERN } from '../../constant';
 import { isMatch } from '../../utils';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader, LoaderCheckOptions } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import ConfigLoader from './config';
 
 @DefineLoader('plugin-config')
 class PluginConfigLoader extends ConfigLoader implements Loader {
 
-  async is(opts: LoaderCheckOptions): Promise<boolean> {
+  static async is(opts: LoaderFindOptions): Promise<boolean> {
     if (this.isConfigDir(opts)) {
       return isMatch(opts.filename, PLUGIN_CONFIG_PATTERN);
     }
diff --git a/src/loader/impl/plugin_meta.ts b/src/loader/impl/plugin_meta.ts
index 37204e4..f1aaa77 100644
--- a/src/loader/impl/plugin_meta.ts
+++ b/src/loader/impl/plugin_meta.ts
@@ -1,8 +1,9 @@
 import { Container } from '@artus/injection';
 import { DefineLoader } from '../decorator';
-import { ManifestItem, Loader } from '../types';
+import { ManifestItem, Loader, LoaderFindOptions } from '../types';
 import { loadMetaFile } from '../../utils/load_meta_file';
 import { PluginMetadata } from '../../plugin/types';
+import { PLUGIN_META } from '../../constant';
 
 @DefineLoader('plugin-meta')
 class PluginMetaLoader implements Loader {
@@ -12,6 +13,10 @@ class PluginMetaLoader implements Loader {
     this.container = container;
   }
 
+  static async is(opts: LoaderFindOptions): Promise<boolean> {
+    return PLUGIN_META.includes(opts.filename);
+  }
+
   async load(item: ManifestItem) {
     const pluginMeta: PluginMetadata = await loadMetaFile<PluginMetadata>(item);
     this.container.set({
diff --git a/src/loader/types.ts b/src/loader/types.ts
index 3eef686..2f8531f 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -5,22 +5,28 @@ interface Manifest {
   relative?: boolean;
 }
 
-interface ManifestItem extends Record<string, any> {
+interface ManifestItem<LoaderState = unknown> extends Record<string, any> {
   path: string;
   extname: string;
   filename: string;
   loader?: string;
   source?: string;
   unitName?: string
+  _loaderState?: LoaderState;
 }
 
-interface LoaderCheckOptions {
+interface LoaderFindOptions {
   filename: string;
   root: string;
   baseDir: string;
   configDir: string;
 }
 
+interface LoaderFindResult {
+  loaderName: string;
+  loaderState?: unknown;
+}
+
 interface LoaderHookUnit {
   before?: Function,
   after?: Function,
@@ -28,9 +34,11 @@ interface LoaderHookUnit {
 
 interface LoaderConstructor {
   new(container: Container): Loader;
+  is?(opts: LoaderFindOptions): Promise<boolean>;
+  onFind?(opts: LoaderFindOptions): Promise<any>;
 };
 interface Loader {
-  is?(opts: LoaderCheckOptions): Promise<boolean>;
+  state?: any;
   load(item: ManifestItem): Promise<void>;
 };
 
@@ -39,7 +47,8 @@ export {
   ManifestItem,
   LoaderHookUnit,
   LoaderConstructor,
-  LoaderCheckOptions,
   Loader,
+  LoaderFindOptions,
+  LoaderFindResult,
 };
 
diff --git a/src/scanner/utils.ts b/src/scanner/utils.ts
index d4bbad7..20fa7df 100644
--- a/src/scanner/utils.ts
+++ b/src/scanner/utils.ts
@@ -49,18 +49,25 @@ export class ScanUtils {
       if (itemStat.isFile()) {
         const filename = path.basename(realPath);
         const filenameWithoutExt = path.basename(realPath, extname);
+        const {
+          loaderName,
+          loaderState,
+        } = await ScanUtils.loaderFactory.findLoader({
+          filename,
+          root,
+          baseDir,
+          configDir
+        });
         const item: ManifestItem = {
           path: options.extensions.includes(extname) ? path.resolve(root, filenameWithoutExt) : realPath,
           extname,
           filename,
-          loader: await ScanUtils.loaderFactory.getLoaderName({
-            filename,
-            root,
-            baseDir,
-            configDir
-          }),
+          loader: loaderName,
           source
         };
+        if (loaderState) {
+          item._loaderState = loaderState;
+        }
         unitName && (item.unitName = unitName);
         const itemList = options.itemMap.get(item.loader ?? DEFAULT_LOADER);
         if (Array.isArray(itemList)) {


Test patch:
diff --git a/test/fixtures/module_with_custom_loader/src/index.ts b/test/fixtures/module_with_custom_loader/src/index.ts
index b46b452..6406d61 100644
--- a/test/fixtures/module_with_custom_loader/src/index.ts
+++ b/test/fixtures/module_with_custom_loader/src/index.ts
@@ -10,7 +10,10 @@ export default ({
       path: path.resolve(rootDir, './test_clazz.ts'),
       extname: '.ts',
       filename: 'test_clazz.ts',
-      loader: 'test-custom-loader'
+      loader: 'test-custom-loader',
+      _loaderState: {
+        hello: 'loaderState'
+      }
     }
   ]
 }) as Manifest;
diff --git a/test/fixtures/module_with_custom_loader/src/loader/test_custom_loader.ts b/test/fixtures/module_with_custom_loader/src/loader/test_custom_loader.ts
index 1d3b229..2b9df1b 100644
--- a/test/fixtures/module_with_custom_loader/src/loader/test_custom_loader.ts
+++ b/test/fixtures/module_with_custom_loader/src/loader/test_custom_loader.ts
@@ -1,15 +1,28 @@
 
-import { DefineLoader, Loader, LoaderCheckOptions, ManifestItem } from '../../../../../src';
+import { DefineLoader, Loader, LoaderFindOptions, ManifestItem } from '../../../../../src';
 import compatibleRequire from '../../../../../src/utils/compatible_require';
 
+interface TestLoaderState {
+  hello: string;
+}
+
 @DefineLoader('test-custom-loader')
 export default class TestCustomLoader implements Loader {
-  async is(opts: LoaderCheckOptions) {
+  static async is(opts: LoaderFindOptions) {
     return opts.filename === 'test_clazz.ts'
   }
 
+  static async onFind(_opts: LoaderFindOptions): Promise<TestLoaderState> {
+    return {
+      hello: 'loaderState'
+    };
+  }
+
+  state!: TestLoaderState;
+
   async load(item: ManifestItem) {
     const clazz = await compatibleRequire(item.path);
     console.log('TestCustomLoader.load ' + clazz.name);
+    console.log('TestCustomLoader.state ' + this.state.hello);
   }
 }
\ No newline at end of file
diff --git a/test/loader.test.ts b/test/loader.test.ts
index be2b3a3..8ab7554 100644
--- a/test/loader.test.ts
+++ b/test/loader.test.ts
@@ -42,7 +42,7 @@ describe('test/loader.test.ts', () => {
       const container = new Container('testDefault');
       const loaderFactory = LoaderFactory.create(container);
 
-      const loaderName = await loaderFactory.getLoaderName({
+      const 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'),
@@ -52,6 +52,7 @@ describe('test/loader.test.ts', () => {
       jest.spyOn(console, 'log');
       await loaderFactory.loadManifest(manifest);
       expect(console.log).toBeCalledWith('TestCustomLoader.load TestClass');
+      expect(console.log).toBeCalledWith('TestCustomLoader.state loaderState');
     });
   });
 });