Repository: artusjs/core
Pull request #28 (2022-04-15 03:56:12)

feat: support framework
定义 `framework` 最终会继承 `ArtusApplication`

完善了 `framework` 对 config 的继承处理，以及提供了相对完善的基于 core 封装的上层协议相关框架，以及使用上层框架的一种用户编程界面，暂不涉及到 `plugin` 逻辑

一种上层框架的协议以及便捷注解方法实现：
* https://github.com/hyj1991/core/tree/master/test/fixtures/frameworks/foo

基于上述协议上层框架对应的用户编程界面：
* https://github.com/hyj1991/core/tree/master/test/fixtures/artus-application

Reference: https://github.com/artusjs/core/issues/7


Patch:
diff --git a/src/application.ts b/src/application.ts
index 9e42a18..5e64940 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -28,6 +28,14 @@ export class ArtusApplication implements Application {
     return this.container.get(ArtusInjectEnum.Config);
   }
 
+  get frameworks(): Record<string, any> {
+    return this.container.get(ArtusInjectEnum.Frameworks);
+  }
+
+  get packages(): Record<string, any> {
+    return this.container.get(ArtusInjectEnum.Packages);
+  }
+
   get trigger(): Trigger {
     return this.container.get(ArtusInjectEnum.Trigger);
   }
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index fee7276..01a9303 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -3,10 +3,14 @@ import { ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV } from '../constraints';
 import { mergeConfig } from '../loader/utils/merge';
 
 type ConfigObject = Record<string, any>;
+type PackageObject = ConfigObject;
+type Framework = { path: string };
 
 @Injectable()
 export default class ConfigurationHandler {
   private configStore: Map<string, ConfigObject> = new Map();
+  private frameworks: Map<string, Framework[]> = new Map();
+  private packages: Map<string, PackageObject[]> = new Map();
 
   getMergedConfig(env?: string): ConfigObject {
     const currentEnv = env ?? process.env[ARTUS_SERVER_ENV] ?? ARTUS_DEFAULT_CONFIG_ENV.DEV;
@@ -19,4 +23,24 @@ export default class ConfigurationHandler {
     const storedConfig = this.configStore.get(env) ?? {};
     this.configStore.set(env, mergeConfig(storedConfig, config));
   }
+
+  getFrameworks(): Map<string, Framework[]> {
+    return this.frameworks;
+  }
+
+  addFramework(source: string, framework: Framework) {
+    const list = this.frameworks.get(source) ?? [];
+    list.push(framework);
+    this.frameworks.set(source, list);
+  }
+
+  getPackages(): Map<string, PackageObject[]> {
+    return this.packages;
+  }
+
+  addPackage(source: string, pkg: PackageObject) {
+    const list = this.packages.get(source) ?? [];
+    list.push(pkg);
+    this.packages.set(source, list);
+  }
 }
diff --git a/src/constraints.ts b/src/constraints.ts
index 123f95e..26f7c90 100644
--- a/src/constraints.ts
+++ b/src/constraints.ts
@@ -5,6 +5,8 @@ export const enum ArtusInjectEnum {
   Trigger = 'artus#trigger',
   LifecycleManager = 'artus#lifecycle-manager',
   Config = 'artus#config',
+  Frameworks = 'artus#frameworks',
+  Packages = 'artus#packages',
   DefaultContainerName = 'artus#default_container',
 }
 
@@ -18,10 +20,12 @@ export enum ARTUS_DEFAULT_CONFIG_ENV {
   DEFAULT = 'default',
 };
 
+export const HOOK_NAME_META_PREFIX = 'hookName:';
 export const HOOK_CONSTRUCTOR_PARAMS = 'appHook:constructorParams';
 export const HOOK_CONSTRUCTOR_PARAMS_APP = 'appHook:constructorParams:app';
 export const HOOK_CONSTRUCTOR_PARAMS_CONTAINER = 'appHook:constructorParams:container';
-export const HOOK_NAME_META_PREFIX = 'hookName:';
+export const HOOK_PARAMS_CONTEXT = 'appHook:constructorParams:context';
+export const HOOK_FILE_LOADER = 'appHook:fileLoader';
 
 export const DEFAULT_EXCLUDES = [
   'test',
@@ -30,14 +34,13 @@ export const DEFAULT_EXCLUDES = [
   'tsconfig*.json',
   '*.d.ts',
   'jest.config.*',
-  'package.json',
   'meta.*',
   'LICENSE'
 ];
 
+export const FRAMEWORK_PATTERN = 'framework.*';
 export const PLUGIN_CONFIG_PATTERN = 'plugin.*';
 export const CONFIG_PATTERN = 'config.*';
-export const EXTENSION_PATTERN = 'app.*';
 export const PLUGIN_META = ['meta.json', 'meta.yaml', 'meta.yml'];
 export const PACKAGE_JSON = 'package.json';
 export const EXCEPTION_FILE = 'artus-exception.yaml';
@@ -48,5 +51,7 @@ export const DEFAULT_LOADER_LIST_WITH_ORDER = [
   'extension',
   'config',
   'plugin-meta',
-  'module'
+  'module',
+  'framework',
+  'package-json',
 ];
diff --git a/src/decorator.ts b/src/decorator.ts
index 9f403cc..6ee4b18 100644
--- a/src/decorator.ts
+++ b/src/decorator.ts
@@ -2,17 +2,20 @@ import {
   HOOK_CONSTRUCTOR_PARAMS,
   HOOK_CONSTRUCTOR_PARAMS_APP,
   HOOK_CONSTRUCTOR_PARAMS_CONTAINER,
-  HOOK_NAME_META_PREFIX
+  HOOK_NAME_META_PREFIX,
+  HOOK_PARAMS_CONTEXT,
+  HOOK_FILE_LOADER,
 } from './constraints';
 
 export function ApplicationExtension(): ClassDecorator {
-  return (_target: any) => {
+  return (target: any) => {
     // Ready to remove?
+    Reflect.defineMetadata(HOOK_FILE_LOADER, { loader: 'extension' }, target);
   };
 };
 
 export function ApplicationHook(hookName?: string): PropertyDecorator {
-  return (target: any, propertyKey: string|symbol) => {
+  return (target: any, propertyKey: string | symbol) => {
     if (typeof propertyKey === 'symbol') {
       throw new Error(`hookName is not support symbol [${propertyKey.description}]`);
     }
@@ -20,20 +23,28 @@ export function ApplicationHook(hookName?: string): PropertyDecorator {
   };
 };
 
-const WithApplicationExtensionConstructorParams = (tag: string): ParameterDecorator => {
-  return (target: any, _propertyKey: string|symbol, parameterIndex: number) => {
+const WithApplicationExtensionParams = (tag: string): ParameterDecorator => {
+  return (target: any, _propertyKey: string | symbol, parameterIndex: number) => {
     const paramsMd = Reflect.getOwnMetadata(HOOK_CONSTRUCTOR_PARAMS, target) ?? [];
     paramsMd[parameterIndex] = tag;
-    Reflect.defineMetadata(HOOK_CONSTRUCTOR_PARAMS, paramsMd, target);
+    if (_propertyKey) {
+      Reflect.defineMetadata(HOOK_CONSTRUCTOR_PARAMS, paramsMd, target[_propertyKey]); // for proto
+    } else {
+      Reflect.defineMetadata(HOOK_CONSTRUCTOR_PARAMS, paramsMd, target); // for constructor
+    }
   };
 }
 
 export function WithApplication(): ParameterDecorator {
-  return WithApplicationExtensionConstructorParams(HOOK_CONSTRUCTOR_PARAMS_APP);
+  return WithApplicationExtensionParams(HOOK_CONSTRUCTOR_PARAMS_APP);
 }
 
 export function WithContainer(): ParameterDecorator {
-  return WithApplicationExtensionConstructorParams(HOOK_CONSTRUCTOR_PARAMS_CONTAINER);
+  return WithApplicationExtensionParams(HOOK_CONSTRUCTOR_PARAMS_CONTAINER);
+}
+
+export function WithContext(): ParameterDecorator {
+  return WithApplicationExtensionParams(HOOK_PARAMS_CONTEXT);
 }
 
 export * from './loader/decorator';
diff --git a/src/framework/handler.ts b/src/framework/handler.ts
new file mode 100644
index 0000000..377ec4c
--- /dev/null
+++ b/src/framework/handler.ts
@@ -0,0 +1,27 @@
+import path from 'path';
+import { ManifestItem } from "../types";
+
+export interface FrameworkConfig {
+  path?: string,
+  package?: string
+}
+
+export class FrameworkHandler {
+  static async handle(root: string, config: FrameworkConfig): Promise<string> {
+    // no framework
+    if (!config.path && !config.package) {
+      return '';
+    }
+
+    try {
+      let baseFrameworkPath = config.path ?? path.dirname(require.resolve(config.package ?? '', { paths: [root] }));
+      return baseFrameworkPath;
+    } catch (err) {
+      throw new Error(`load framework faild: ${err}, framework config: ${JSON.stringify(config)}`);
+    }
+  }
+
+  static serialize(list: ManifestItem[]): string[] {
+    return list.map(item => item.path);
+  }
+}
diff --git a/src/framework/index.ts b/src/framework/index.ts
new file mode 100644
index 0000000..68ae53f
--- /dev/null
+++ b/src/framework/index.ts
@@ -0,0 +1,1 @@
+export * from './handler';
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index f0d19eb..f720bdc 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -4,6 +4,11 @@ import { Manifest, ManifestItem, LoaderConstructor, LoaderHookUnit } from './typ
 import ConfigurationHandler from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 
+export const configSet = {
+  framework: new Set<string>(),
+  plugin: new Set<string>(),
+};
+
 export class LoaderFactory {
   private container: Container;
   private static loaderClazzMap: Map<string, LoaderConstructor> = new Map();
@@ -12,7 +17,7 @@ export class LoaderFactory {
     this.loaderClazzMap.set(loaderName, clazz);
   }
 
-  constructor (container: Container) {
+  constructor(container: Container) {
     this.container = container;
   }
 
@@ -34,6 +39,18 @@ export class LoaderFactory {
           });
           lifecycleManager.emitHook('configDidLoad');
         }
+      },
+      framework: {
+        after: () => this.container.set({
+          id: ArtusInjectEnum.Frameworks,
+          value: configurationHandler.getFrameworks()
+        })
+      },
+      'package-json': {
+        after: () => this.container.set({
+          id: ArtusInjectEnum.Packages,
+          value: configurationHandler.getPackages()
+        })
       }
     });
   }
diff --git a/src/loader/impl/framework.ts b/src/loader/impl/framework.ts
new file mode 100644
index 0000000..934be9e
--- /dev/null
+++ b/src/loader/impl/framework.ts
@@ -0,0 +1,22 @@
+import { Container } from '@artus/injection';
+import ConfigurationHandler from '../../configuration';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
+
+@DefineLoader('framework')
+class FrameworkLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const originConfigObj = await compatibleRequire(item.path);
+    const configHandler = this.container.get(ConfigurationHandler);
+    configHandler.addFramework(item.source || 'app', originConfigObj);
+  }
+}
+
+export default FrameworkLoader;
diff --git a/src/loader/impl/index.ts b/src/loader/impl/index.ts
index 747a318..b6c93c6 100644
--- a/src/loader/impl/index.ts
+++ b/src/loader/impl/index.ts
@@ -4,3 +4,5 @@ export * from './exception';
 export * from './extension';
 export * from './pluginMeta';
 export * from './pluginConfig';
+export * from './framework';
+export * from './package';
diff --git a/src/loader/impl/package.ts b/src/loader/impl/package.ts
new file mode 100644
index 0000000..af889a2
--- /dev/null
+++ b/src/loader/impl/package.ts
@@ -0,0 +1,22 @@
+import { Container } from '@artus/injection';
+import ConfigurationHandler from '../../configuration';
+import { DefineLoader } from '../decorator';
+import { ManifestItem, Loader } from '../types';
+import compatibleRequire from '../../utils/compatible-require';
+
+@DefineLoader('package-json')
+class PackageLoader implements Loader {
+  private container: Container;
+
+  constructor(container) {
+    this.container = container;
+  }
+
+  async load(item: ManifestItem) {
+    const originConfigObj = await compatibleRequire(item.path);
+    const configHandler = this.container.get(ConfigurationHandler);
+    configHandler.addPackage(item.source || 'app', originConfigObj);
+  }
+}
+
+export default PackageLoader;
diff --git a/src/scanner/scan.ts b/src/scanner/scan.ts
index f8397ed..7775cce 100644
--- a/src/scanner/scan.ts
+++ b/src/scanner/scan.ts
@@ -10,9 +10,11 @@ import {
     PLUGIN_CONFIG_PATTERN,
     PLUGIN_META,
     EXCEPTION_FILE,
-    EXTENSION_PATTERN,
+    FRAMEWORK_PATTERN,
+    PACKAGE_JSON,
     DEFAULT_LOADER_LIST_WITH_ORDER,
-    DEFAULT_LOADER
+    DEFAULT_LOADER,
+    HOOK_FILE_LOADER,
 } from '../constraints';
 import {
     ScannerOptions,
@@ -20,6 +22,7 @@ import {
 import { Manifest, ManifestItem } from '../loader';
 import ConfigurationHandler from '../configuration';
 import { PluginFactory } from '../plugin';
+import { FrameworkHandler } from '../framework';
 
 export class Scanner {
     private options: ScannerOptions;
@@ -40,33 +43,55 @@ export class Scanner {
 
     public async scan(root: string) {
         // 0. Scan Application
-        await this.walk(root);
-    
+        await this.walk(root, 'app');
+
         // 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';
+            const pluginConfig = await compatibleRequire(pluginConfigFile.path);
+            if (pluginConfig) {
+                let [_, env, extname] = pluginConfigFile.filename.split('.');
+                if (!extname) {
+                    env = 'default';
+                }
+                pluginConfigHandler.setConfig(env, pluginConfig);
             }
-            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 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',
+                unitName: plugin.name,
+            });
+            await this.walk(plugin.importPath, 'plugin', plugin.name);
         }
+
+        // 2. Scan Frameworks
+        const serialize = FrameworkHandler.serialize;
+        const frameworkMap = new Map<string, boolean>();
+        const frameworks: string[] = [];
+        frameworks.push(...serialize(this.itemMap.get('framework') ?? []));
+        frameworks.push(...serialize(this.itemMap.get('package-json') ?? []));
+        let frameworkBaseDir = root;
+        for (const frame of frameworks) {
+            if (frameworkMap.get(frame)) {
+                continue;
+            }
+            frameworkMap.set(frame, true);
+            const frameworkConfig = await compatibleRequire(frame);
+            frameworkConfig.framework && (frameworkConfig.package = frameworkConfig.framework);
+            const baseFrameworkPath = await FrameworkHandler.handle(frameworkBaseDir, frameworkConfig);
+            baseFrameworkPath && (frameworkBaseDir = baseFrameworkPath) && await this.walk(baseFrameworkPath, 'framework');
+            frameworks.push(...serialize(this.itemMap.get('framework') ?? []));
+            frameworks.push(...serialize(this.itemMap.get('package-json') ?? []));
+        }
+
         const result: Manifest = {
             items: this.getItemsFromMap(),
         };
@@ -76,7 +101,7 @@ export class Scanner {
         return result;
     }
 
-    private async walk(root: string, unitName: string = this.options.appName!) {
+    private async walk(root: string, source: string, unitName: string = '') {
         if (!existsSync(root)) {
             return;
         }
@@ -100,7 +125,7 @@ export class Scanner {
                 if (this.exist(realPath, PLUGIN_META)) {
                     continue;
                 }
-                await this.walk(realPath);
+                await this.walk(realPath, source, unitName);
                 continue;
             }
 
@@ -111,9 +136,10 @@ export class Scanner {
                     path: this.moduleExtensions.includes(extname) ? path.resolve(root, filenameWithoutExt) : realPath,
                     extname,
                     filename,
-                    loader: this.getLoaderName(filename),
-                    source: unitName
+                    loader: await this.getLoaderName(root, filename),
+                    source
                 };
+                unitName && (item.unitName = unitName);
                 const itemList = this.itemMap.get(item.loader ?? DEFAULT_LOADER);
                 if (Array.isArray(itemList)) {
                     itemList.unshift(item);
@@ -130,16 +156,31 @@ export class Scanner {
         return items;
     }
 
-    private getLoaderName(filename: string): string {
-        // TODO: 后面考虑重构通过参数传递匹配规则
+    private async getLoaderName(root: string, filename: string): Promise<string> {
+        // package.json
+        if (this.isPakcageJson(filename)) {
+            return 'package-json';
+        }
+
+        // artus-exception.yaml
+        if (this.isException(filename)) {
+            return 'exception';
+        }
+
+        // get loader from reflect metadata
+        const target = await compatibleRequire(path.join(root, filename));
+        const metadata = Reflect.getMetadata(HOOK_FILE_LOADER, target);
+        if (metadata?.loader) {
+            return metadata.loader;
+        }
+
+        // TODO: wait for refactor
         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 if (this.isFramework(filename)) {
+            return 'framework';
         } else {
             return 'module';
         }
@@ -180,8 +221,12 @@ export class Scanner {
     }
 
     // TODO:
-    private isExtension(filename: string,): boolean {
-        return isMatch(filename, EXTENSION_PATTERN);
+    private isFramework(filename: string): boolean {
+        return isMatch(filename, FRAMEWORK_PATTERN);
+    }
+
+    private isPakcageJson(filename: string): boolean {
+        return isMatch(filename, PACKAGE_JSON);
     }
 
     private exist(dir: string, filenames: string[]): boolean {
@@ -193,4 +238,4 @@ export class Scanner {
     private async writeFile(filename: string = 'manifest.json', data: string) {
         await fs.writeFile(filename, data);
     }
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/fixtures/artus-application/package.json b/test/fixtures/artus-application/package.json
new file mode 100644
index 0000000..e3c6557
--- /dev/null
+++ b/test/fixtures/artus-application/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "artus-application"
+}
diff --git a/test/fixtures/artus-application/src/config/config.default.ts b/test/fixtures/artus-application/src/config/config.default.ts
new file mode 100644
index 0000000..4ad00b5
--- /dev/null
+++ b/test/fixtures/artus-application/src/config/config.default.ts
@@ -0,0 +1,1 @@
+export const name = 'from application';
diff --git a/test/fixtures/artus-application/src/config/framework.ts b/test/fixtures/artus-application/src/config/framework.ts
new file mode 100644
index 0000000..7b001fb
--- /dev/null
+++ b/test/fixtures/artus-application/src/config/framework.ts
@@ -0,0 +1,5 @@
+import path from 'path';
+
+export default {
+  path: path.resolve(__dirname, '../../../frameworks/bar')
+};
diff --git a/test/fixtures/artus-application/src/controller/hello.ts b/test/fixtures/artus-application/src/controller/hello.ts
new file mode 100644
index 0000000..306f9c7
--- /dev/null
+++ b/test/fixtures/artus-application/src/controller/hello.ts
@@ -0,0 +1,15 @@
+import { Context } from '@artus/pipeline';
+import { HttpController, HttpMethod, HTTPMethodEnum } from '../../../frameworks/foo/src/trigger/http';
+import { WithContext } from '../../../../../src/decorator';
+
+@HttpController()
+export default class Hello {
+  @HttpMethod({
+    method: HTTPMethodEnum.GET,
+    path: '/home'
+  })
+  async index(@WithContext() ctx: Context) {
+    const { params: { config } } = ctx.input;
+    return { title: `Hello Artus ${config.name}` };
+  }
+};
diff --git a/test/fixtures/artus-application/src/index.ts b/test/fixtures/artus-application/src/index.ts
new file mode 100644
index 0000000..69c98fb
--- /dev/null
+++ b/test/fixtures/artus-application/src/index.ts
@@ -0,0 +1,12 @@
+import { Manifest } from "../../../../src";
+import { FrameworkBar } from '../../frameworks/bar/src';
+
+export default class MyArtusApplication extends FrameworkBar {
+}
+
+export async function main(manifest: Manifest) {
+  const app = new MyArtusApplication();
+  await app.load(manifest);
+  await app.run();
+  return app;
+}
diff --git a/test/fixtures/frameworks/bar/package.json b/test/fixtures/frameworks/bar/package.json
new file mode 100644
index 0000000..694594c
--- /dev/null
+++ b/test/fixtures/frameworks/bar/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "bar"
+}
diff --git a/test/fixtures/frameworks/bar/src/config/config.default.ts b/test/fixtures/frameworks/bar/src/config/config.default.ts
new file mode 100644
index 0000000..6f1dbda
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/config/config.default.ts
@@ -0,0 +1,3 @@
+export const port = 3003;
+
+export const name = 'from framework: bar';
diff --git a/test/fixtures/frameworks/bar/src/config/framework.ts b/test/fixtures/frameworks/bar/src/config/framework.ts
new file mode 100644
index 0000000..1956f85
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/config/framework.ts
@@ -0,0 +1,5 @@
+import path from 'path';
+
+export default {
+  path: path.join(__dirname, '../../../foo')
+}
diff --git a/test/fixtures/frameworks/bar/src/index.ts b/test/fixtures/frameworks/bar/src/index.ts
new file mode 100644
index 0000000..2938556
--- /dev/null
+++ b/test/fixtures/frameworks/bar/src/index.ts
@@ -0,0 +1,5 @@
+import { FrameworkFoo as Foo } from '../../foo/src';
+
+export class FrameworkBar extends Foo {
+
+}
diff --git a/test/fixtures/frameworks/foo/package.json b/test/fixtures/frameworks/foo/package.json
new file mode 100644
index 0000000..bde99de
--- /dev/null
+++ b/test/fixtures/frameworks/foo/package.json
@@ -0,0 +1,3 @@
+{
+  "name": "foo"
+}
diff --git a/test/fixtures/frameworks/foo/src/config/config.default.ts b/test/fixtures/frameworks/foo/src/config/config.default.ts
new file mode 100644
index 0000000..5f95d01
--- /dev/null
+++ b/test/fixtures/frameworks/foo/src/config/config.default.ts
@@ -0,0 +1,3 @@
+export const port = 3002;
+
+export const name = 'from framework: foo';
diff --git a/test/fixtures/frameworks/foo/src/index.ts b/test/fixtures/frameworks/foo/src/index.ts
new file mode 100644
index 0000000..c8d5ed0
--- /dev/null
+++ b/test/fixtures/frameworks/foo/src/index.ts
@@ -0,0 +1,8 @@
+import { ArtusApplication } from '../../../../../src';
+import { server } from './lifecycle';
+
+export class FrameworkFoo extends ArtusApplication {
+  isListening() {
+    return server?.listening;
+  }
+}
diff --git a/test/fixtures/frameworks/foo/src/lifecycle.ts b/test/fixtures/frameworks/foo/src/lifecycle.ts
new file mode 100644
index 0000000..a7358d7
--- /dev/null
+++ b/test/fixtures/frameworks/foo/src/lifecycle.ts
@@ -0,0 +1,42 @@
+import { Server } from 'http';
+import http from 'http';
+import { ApplicationExtension, ApplicationHook, WithApplication } from '../../../../../src/decorator';
+import { ApplicationLifecycle } from '../../../../../src/types';
+import { Input } from '@artus/pipeline';
+import { ArtusApplication } from '../../../../../src';
+import HttpTrigger, { registerController } from './trigger/http';
+
+export let server: Server;
+
+@ApplicationExtension()
+export default class ApplicationHookExtension implements ApplicationLifecycle {
+  app: ArtusApplication;
+
+  constructor(@WithApplication() app: ArtusApplication) {
+    this.app = app;
+  }
+
+  @ApplicationHook()
+  async didLoad() {
+    // register controller
+    registerController(this.app.trigger as HttpTrigger);
+  }
+
+  @ApplicationHook()
+  willReady() {
+    const config = this.app.config ?? {};
+    const port = config.port;
+    server = http
+      .createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
+        const input = new Input();
+        input.params = { req, res, config };
+        await this.app.trigger.startPipeline(input);
+      })
+      .listen(port);
+  }
+
+  @ApplicationHook()
+  beforeClose() {
+    server?.close();
+  }
+}
diff --git a/test/fixtures/frameworks/foo/src/trigger/http.ts b/test/fixtures/frameworks/foo/src/trigger/http.ts
new file mode 100644
index 0000000..4764aad
--- /dev/null
+++ b/test/fixtures/frameworks/foo/src/trigger/http.ts
@@ -0,0 +1,112 @@
+import 'reflect-metadata';
+import { Stream } from 'stream';
+import { Context, Next } from '@artus/pipeline';
+import { Trigger } from '../../../../../../src';
+import { DefineTrigger } from '../../../../../../src/decorator';
+import { Constructable } from '@artus/injection';
+import { HOOK_CONSTRUCTOR_PARAMS, HOOK_PARAMS_CONTEXT } from '../../../../../../src/constraints';
+import { Injectable, ScopeEnum } from '@artus/injection';
+
+export const enum HTTPMethodEnum {
+  GET = 'GET',
+  POST = 'POST',
+  DELETE = ' DELETE',
+  PUT = 'PUT',
+};
+
+export type ControllerParams = {
+  path?: string
+};
+
+export type HttpParams = {
+  method: HTTPMethodEnum,
+  path: string
+};
+
+export type ControllerMeta = {
+  prefix: string,
+  clazz: Constructable
+}
+
+export const controllerMap = new Set<ControllerMeta>();
+
+export const HOOK_HTTP_META_PREFIX = 'ARTUS#HOOK_HTTP_META_PREFIX::';
+
+@DefineTrigger()
+export default class HttpTrigger extends Trigger {
+  constructor() {
+    super();
+    // first of all
+    this.use(async (ctx: Context, next: Next) => {
+      await next();
+      await this.respond(ctx);
+    });
+  }
+
+  async respond(ctx: Context) {
+    const { res } = ctx.input.params;
+    const { data } = ctx.output;
+
+    res.status = data.status || 200;
+    const { content } = data;
+
+    if (Buffer.isBuffer(content) || typeof content === 'string') {
+      return res.end(content);
+    }
+
+    if (content instanceof Stream) {
+      return content.pipe(res);
+    }
+
+    return res.end(JSON.stringify(content));
+  }
+}
+
+export function HttpController(options?: ControllerParams): ClassDecorator {
+  const prefix = options?.path ?? '';
+  return (target: any) => {
+    controllerMap.add({ prefix, clazz: target });
+  };
+}
+
+export function HttpMethod(options: HttpParams): PropertyDecorator {
+  return (target: any, propertyKey: string | symbol) => {
+    if (typeof propertyKey === 'symbol') {
+      throw new Error(`http hookName is not support symbol [${propertyKey.description}]`);
+    }
+    Reflect.defineMetadata(`${HOOK_HTTP_META_PREFIX}${propertyKey}`, options, target.constructor);
+    Injectable({ scope: ScopeEnum.EXECUTION })(target);
+  };
+}
+
+export function registerController(trigger: HttpTrigger) {
+  for (const controller of controllerMap) {
+    const { prefix, clazz } = controller;
+    const fnMetaKeys = Reflect.getMetadataKeys(clazz);
+
+    for (let key of fnMetaKeys) {
+      if (!key.startsWith(HOOK_HTTP_META_PREFIX)) {
+        continue;
+      }
+
+      // register controller
+      const { method, path } = Reflect.getMetadata(key, clazz);
+      key = key.replace(HOOK_HTTP_META_PREFIX, '');
+
+      // match router
+      trigger.use(async (ctx: Context, next: Next) => {
+        const { input: { params: { req } } } = ctx;
+        if (req.url === `${prefix}${path}` && req.method === method) {
+          const instance: any = ctx.container.get(clazz);
+          const target = instance[key];
+          const params: any = Reflect.getMetadata(HOOK_CONSTRUCTOR_PARAMS, target) ?? [];
+          const paramsMap = {
+            [HOOK_PARAMS_CONTEXT]: ctx
+          };
+          ctx.output.data.content = await target(...params.map((param) => paramsMap[param]));
+        }
+        await next();
+      });
+    }
+  }
+}
diff --git a/test/framework.test.ts b/test/framework.test.ts
new file mode 100644
index 0000000..3213bcc
--- /dev/null
+++ b/test/framework.test.ts
@@ -0,0 +1,28 @@
+import 'reflect-metadata';
+import { Scanner } from '../src/scanner';
+import path from 'path';
+import axios from 'axios';
+import assert from 'assert';
+
+describe('test/framework.test.ts', () => {
+  it('should run succeed', async () => {
+    const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'] });
+    const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/artus-application'));
+    // console.log('manifest', manifest);
+    const { main } = await import('./fixtures/artus-application/src');
+    const app = await main(manifest);
+    assert(app.frameworks.get('app').length === 1);
+    assert(app.frameworks.get('framework').length === 1);
+    assert(app.packages.get('app').length === 1);
+    assert(app.packages.get('framework').length === 2);
+    assert(app.isListening());
+    const port = app.config?.port;
+    assert(port === 3003);
+    const testResponse = await axios.get(`http://127.0.0.1:${port}/home`);
+    assert(testResponse.status === 200);
+    assert(testResponse.data.title === 'Hello Artus from application');
+    await app.close();
+    assert(!app.isListening());
+
+  });
+});
diff --git a/test/scanner.test.ts b/test/scanner.test.ts
index 598de23..81a5e63 100644
--- a/test/scanner.test.ts
+++ b/test/scanner.test.ts
@@ -8,7 +8,7 @@ describe('test/scanner.test.ts', () => {
         const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app-koa-with-ts'));
         expect(manifest).toBeDefined();
         expect(manifest.items).toBeDefined();
-        expect(manifest.items.length).toBe(11);
+        expect(manifest.items.length).toBe(12);
 
         expect(manifest.items.filter(item => item.loader === 'plugin-config').length).toBe(1);
         expect(manifest.items.filter(item => item.loader === 'plugin-meta').length).toBe(1);
@@ -17,8 +17,8 @@ describe('test/scanner.test.ts', () => {
         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 === 'mysql').length).toBe(0);
-        expect(manifest.items.filter(item => item.source === 'app').length).toBe(9);
+        expect(manifest.items.filter(item => item.unitName === 'redis').length).toBe(2);
+        expect(manifest.items.filter(item => item.unitName === 'mysql').length).toBe(0);
+        expect(manifest.items.filter(item => item.source === 'app').length).toBe(10);
     });
-});
\ No newline at end of file
+});