Repository: artusjs/core
Pull request #50 (2022-05-05 05:53:00)

refactor: scan files by env
* 启动器更改为 Env 无关（除了 config），移除了 Env 过滤 Manifest item 相关的逻辑
* 扫描器更改为 Env 有关，允许传入多个 Env 获取对应的多份 Manifest（格式参见 [RFC](https://github.com/artusjs/core/issues/49)）
* Plugin 的实现还原为之前的逻辑

close https://github.com/artusjs/core/issues/49


Patch:
diff --git a/src/application.ts b/src/application.ts
index d183815..9e0a669 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -19,7 +19,7 @@ export class ArtusApplication implements Application {
   constructor(opts?: ApplicationInitOptions) {
     this.container = new Container(opts?.containerName ?? ArtusInjectEnum.DefaultContainerName);
     this.lifecycleManager = new LifecycleManager(this, this.container);
-    this.loaderFactory = LoaderFactory.create(this.container, opts?.envUnits);
+    this.loaderFactory = LoaderFactory.create(this.container);
 
     process.on('SIGINT', () => this.close(true));
     process.on('SIGTERM', () => this.close(true));
@@ -75,10 +75,6 @@ export class ArtusApplication implements Application {
     // Load user manifest
     this.manifest = manifest;
 
-    // load env units
-    await this.loaderFactory.loadEnvUnits(manifest);
-
-    // load other files
     await this.loaderFactory.loadManifest(manifest);
 
     await this.lifecycleManager.emitHook('didLoad');
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index 594cb59..bc8a3f9 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -1,19 +1,17 @@
 import { Injectable } from '@artus/injection';
 import { ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV } from '../constraints';
 import { mergeConfig } from '../loader/utils/merge';
-import { BasePlugin, PluginFactory } from '../plugin';
-import { PluginConfigItem } from '../plugin/types';
 import { DefineConfigHandle } from './decorator';
 
 export type ConfigObject = Record<string, any>;
+export type FrameworkObject = { path: string, env: string };
 export type PackageObject = ConfigObject;
-export type EnvUnit = { path: string, choose?: string, ignore?: EnvUnit[] };
 export type FrameworkOptions = { env: string, unitName: string };
 
 @Injectable()
 export default class ConfigurationHandler {
   private configStore: Map<string, ConfigObject> = new Map();
-  private frameworks: Map<string, EnvUnit[]> = new Map();
+  private frameworks: Map<string, FrameworkObject[]> = new Map();
   private packages: Map<string, PackageObject[]> = new Map();
 
   getMergedConfig(env?: string): ConfigObject {
@@ -28,85 +26,31 @@ export default class ConfigurationHandler {
     this.configStore.set(env, mergeConfig(storedConfig, config));
   }
 
-  @DefineConfigHandle('plugin-config')
-  async getPluginConfig(env?: string): Promise<Map<string, EnvUnit>> {
-    const currentEnv = env ?? process.env[ARTUS_SERVER_ENV] ?? ARTUS_DEFAULT_CONFIG_ENV.DEV;
-    const mergedConfig = this.getMergedConfig(currentEnv)?.plugin;
-    const pluginSortedList = await PluginFactory.createFromConfig(mergedConfig || {});
-
-    // get all plugins
-    const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
-    for (const rawEnv of this.configStore.keys()) {
-      const config: any = this.configStore.get(rawEnv) ?? {};
-      const pluginConfig: Record<string, PluginConfigItem> = config.plugin;
-      if (!pluginConfig) {
-        continue;
-      }
-
-      for (const [name, config] of Object.entries(pluginConfig)) {
-        if (!BasePlugin.checkGetPluginConfig(name, config as any, false)) {
-          continue;
-        }
-
-        const items = pluginsConfig.get(name);
-        if (Array.isArray(items)) {
-          items.push(config);
-          continue;
-        }
-        pluginsConfig.set(name, [config]);
-      }
-    }
-
-    const allPlugins: BasePlugin[] = await PluginFactory.createFromConfigList(pluginsConfig);
-
-    const pluginMap = new Map<string, EnvUnit>();
-    for (const plugin of pluginSortedList) {
-      const unit: EnvUnit = { path: plugin.importPath, choose: currentEnv, ignore: [] };
-
-      // ignore disabled
-      if (!plugin.enable) {
-        unit.ignore?.push(unit);
-      }
-
-      // ignore others
-      const list = allPlugins
-        .filter(plg => plg.name === plugin.name)
-        .filter(plg => plg.importPath !== plugin.importPath)
-        .map(plg => ({ path: plg.importPath }));
-      unit.ignore?.push(...list);
-
-      pluginMap.set(plugin.name, unit);
-    }
-
-    return pluginMap;
-  }
-
   @DefineConfigHandle('framework-config')
-  async getFrameworkConfig(
+  getFrameworkConfig(
     env?: string,
     key = 'app',
-    frameworkMap = new Map<string, EnvUnit>()): Promise<Map<string, EnvUnit>> {
+    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 EnvUnit[];
-    const envList = list.filter(item => item.choose === currentEnv);
-    const defaultList = list.filter(item => item.choose === ARTUS_DEFAULT_CONFIG_ENV.DEFAULT);
-    const result = envList.length ? envList[0] : defaultList.length ? defaultList[0] : undefined;
-    if (!result) {
-      return frameworkMap;
+    const list = this.frameworks.get(key) as unknown as FrameworkObject[];
+    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);
+
+    if (config.path) {
+      this.getFrameworkConfig(env, config.path, frameworkMap);
     }
-    result.ignore = list.filter(item => item.path !== result.path);
-    frameworkMap.set(key, result);
-    this.getFrameworkConfig(currentEnv, result?.path, frameworkMap);
     return frameworkMap;
   }
 
-  addFramework(source: string, framework: EnvUnit, options: FrameworkOptions) {
+  addFramework(source: string, framework: FrameworkObject, options: FrameworkOptions) {
     const key = options.unitName || source;
     const list = this.frameworks.get(key) ?? [];
-    framework.choose = options.env;
+    framework.env = options.env;
     list.push(framework);
     this.frameworks.set(key, list);
   }
diff --git a/src/framework/handler.ts b/src/framework/handler.ts
index 377ec4c..4fe8716 100644
--- a/src/framework/handler.ts
+++ b/src/framework/handler.ts
@@ -1,5 +1,7 @@
 import path from 'path';
+import ConfigurationHandler, { ConfigObject } from '../configuration';
 import { ManifestItem } from "../types";
+import compatibleRequire from '../utils/compatible-require';
 
 export interface FrameworkConfig {
   path?: string,
@@ -7,6 +9,25 @@ export interface FrameworkConfig {
 }
 
 export class FrameworkHandler {
+  static async mergeConfig(env: string, frameworks: ManifestItem[], done: string[]): Promise<{ config: ConfigObject, done: string[] }> {
+    frameworks = frameworks.filter(item => !done.includes(item.path));
+    const frameworkConfigHandler = new ConfigurationHandler();
+    for (const frameworkConfigFile of frameworks) {
+      const frameworkConfig = await compatibleRequire(frameworkConfigFile.path);
+      if (frameworkConfig) {
+        let [_, env, extname] = frameworkConfigFile.filename.split('.');
+        if (!extname) {
+          env = 'default';
+        }
+        frameworkConfigHandler.setConfig(env, frameworkConfig);
+      }
+    }
+
+    done = done.concat(frameworks.map(item => item.path));
+
+    return { config: frameworkConfigHandler.getMergedConfig(env), done };
+  }
+
   static async handle(root: string, config: FrameworkConfig): Promise<string> {
     // no framework
     if (!config.path && !config.package) {
@@ -20,8 +41,4 @@ export class FrameworkHandler {
       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/loader/factory.ts b/src/loader/factory.ts
index cb43ced..0bcaca5 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,82 +1,29 @@
 import { Container } from '@artus/injection';
-import { ArtusInjectEnum, ArtusInjectPrefix, DEFAULT_LOADER, HOOK_CONFIG_HANDLE } from '../constraints';
+import { ArtusInjectEnum, DEFAULT_LOADER } from '../constraints';
 import { Manifest, ManifestItem, LoaderConstructor, LoaderHookUnit } from './types';
-import ConfigurationHandler, { EnvUnit } from '../configuration';
+import ConfigurationHandler from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 
 export class LoaderFactory {
   private container: Container;
-  private envUnits: string[];
   private static loaderClazzMap: Map<string, LoaderConstructor> = new Map();
 
   static registerLoader(loaderName: string, clazz: LoaderConstructor) {
     this.loaderClazzMap.set(loaderName, clazz);
   }
 
-  constructor(container: Container, envUnits: string[] = []) {
+  constructor(container: Container) {
     this.container = container;
-    this.envUnits = [
-      ...envUnits,
-      'plugin-config',
-      'framework-config'
-    ]
   }
 
-  static create(container: Container, envUnits: string[] = []): LoaderFactory {
-    return new LoaderFactory(container, envUnits);
-  }
-
-  async setContainer(envUnit: string) {
-    const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
-    const propertyKey = Reflect.getMetadata(`${HOOK_CONFIG_HANDLE}${envUnit}`, ConfigurationHandler);
-    this.container.set({
-      id: `${ArtusInjectPrefix}${envUnit}`,
-      value: await configurationHandler[propertyKey]()
-    })
-  }
-
-  async loadEnvUnits(manifest: Manifest): Promise<void> {
-    const items = manifest.items.filter(item => this.envUnits.includes(item.loader ?? ''));
-
-    // set loader hook
-    const loaderHooks = {};
-    for (const envUnit of this.envUnits) {
-      await this.setContainer(envUnit);
-      loaderHooks[envUnit] = {
-        after: async () => await this.setContainer(envUnit)
-      };
-    }
-
-    await this.loadItemList(items, loaderHooks);
-  };
-
-  async filterUnusedFilesByEnv(manifest: Manifest): Promise<Manifest> {
-    const ignoreFiles: string[] = [];
-    for (const envUnit of this.envUnits) {
-      const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
-      const propertyKey = Reflect.getMetadata(`${HOOK_CONFIG_HANDLE}${envUnit}`, ConfigurationHandler);
-      const units: Map<string, EnvUnit> = await configurationHandler[propertyKey]();
-      for (const [, { ignore }] of units.entries()) {
-        if (!ignore) {
-          continue;
-        }
-        ignoreFiles.push(...ignore.map(item => item.path));
-      }
-    }
-
-    manifest.items = manifest.items.filter(item =>
-      !this.envUnits.includes(item.loader ?? '')
-      && ignoreFiles.every(ignoreFile => !item.path.startsWith(ignoreFile))
-    );
-
-    return manifest;
+  static create(container: Container): LoaderFactory {
+    return new LoaderFactory(container);
   }
 
   async loadManifest(manifest: Manifest): Promise<void> {
     const lifecycleManager: LifecycleManager = this.container.get(ArtusInjectEnum.LifecycleManager);
     const configurationHandler: ConfigurationHandler = this.container.get(ConfigurationHandler);
 
-    manifest = await this.filterUnusedFilesByEnv(manifest);
     await this.loadItemList(manifest.items, {
       config: {
         before: () => lifecycleManager.emitHook('configWillLoad'),
@@ -88,6 +35,12 @@ export class LoaderFactory {
           lifecycleManager.emitHook('configDidLoad');
         }
       },
+      'framework-config': {
+        after: () => this.container.set({
+          id: ArtusInjectEnum.Frameworks,
+          value: configurationHandler.getFrameworkConfig()
+        })
+      },
       'package-json': {
         after: () => this.container.set({
           id: ArtusInjectEnum.Packages,
diff --git a/src/plugin/base.ts b/src/plugin/base.ts
index 54d98eb..db83659 100644
--- a/src/plugin/base.ts
+++ b/src/plugin/base.ts
@@ -1,5 +1,5 @@
 import { Plugin, PluginConfigItem, PluginMetadata } from "./types";
-type PluginsMap = Map<string, BasePlugin[]>;
+type PluginMap = Map<string, BasePlugin>;
 
 export class BasePlugin implements Plugin {
   public name: string;
@@ -10,31 +10,23 @@ export class BasePlugin implements Plugin {
 
   constructor(name: string, configItem: PluginConfigItem) {
     this.name = name;
-
-    this.importPath = BasePlugin.checkGetPluginConfig(name, configItem);
-    this.enable = configItem.enable ?? false;
-  }
-
-  async init() { }
-
-  static checkGetPluginConfig(name: string, configItem: PluginConfigItem, shouldThrow: boolean = true): string {
     let importPath = configItem.path ?? '';
     if (configItem.package) {
       importPath = require.resolve(configItem.package);
     }
     if (!importPath) {
-      if (shouldThrow) {
-        throw new Error(`Plugin ${name} need have path or package field`);
-      }
-      return '';
+      throw new Error(`Plugin ${name} need have path or package field`);
     }
-    return importPath;
+    this.importPath = importPath;
+    this.enable = configItem.enable ?? false;
   }
 
-  checkDepExisted(pluginsMap: PluginsMap) {
+  async init() { }
+
+  checkDepExisted(pluginMap: PluginMap) {
     for (const { name: pluginName, optional } of this.metadata.dependencies ?? []) {
-      const instances = pluginsMap.get(pluginName) ?? [];
-      if (!instances.length || instances.every(instance => !instance.enable)) {
+      const instance = pluginMap.get(pluginName);
+      if (!instance || !instance.enable) {
         if (optional) {
           // TODO: use artus logger instead
           console.warn(`Plugin ${this.name} need have optional dependence: ${pluginName}.`)
diff --git a/src/plugin/common.ts b/src/plugin/common.ts
index dd71658..7931b1b 100644
--- a/src/plugin/common.ts
+++ b/src/plugin/common.ts
@@ -1,7 +1,7 @@
 import { Plugin } from './types';
 
 // A utils function that toplogical sort plugins
-export function topologicalSort(pluginInstancesMap: Map<string, Plugin[]>, pluginDepEdgeList: [string, string][]): string[] {
+export function topologicalSort(pluginInstanceMap: Map<string, Plugin>, pluginDepEdgeList: [string, string][]): string[] {
   const res: string[] = [];
   const indegree: Map<string, number> = new Map();
 
@@ -11,7 +11,7 @@ export function topologicalSort(pluginInstancesMap: Map<string, Plugin[]>, plugi
 
   const queue: string[] = [];
 
-  for (const [name] of pluginInstancesMap) {
+  for (const [name] of pluginInstanceMap) {
     if (!indegree.has(name)) {
       queue.push(name);
     }
diff --git a/src/plugin/factory.ts b/src/plugin/factory.ts
index c662cf9..82c8d3f 100644
--- a/src/plugin/factory.ts
+++ b/src/plugin/factory.ts
@@ -10,40 +10,26 @@ export class PluginFactory {
     return pluginInstance;
   }
 
-  static async createFromConfigList(config: Map<string, PluginConfigItem[]>): Promise<BasePlugin[]> {
-    const pluginInstancesMap: Map<string, BasePlugin[]> = new Map();
-    for (const [name, items] of config.entries()) {
-      const pluginInstance = await Promise.all(items.map(item => PluginFactory.create(name, item)));
-      pluginInstancesMap.set(name, pluginInstance);
+  static async createFromConfig(config: Record<string, PluginConfigItem>): Promise<BasePlugin[]> {
+    const pluginInstanceMap: Map<string, BasePlugin> = new Map();
+    for (const [name, item] of Object.entries(config)) {
+      const pluginInstance = await PluginFactory.create(name, item);
+      if (pluginInstance.enable) {
+        pluginInstanceMap.set(name, pluginInstance);
+      }
     }
     let pluginDepEdgeList: [string, string][] = [];
     // Topological sort plugins
-    for (const [_name, pluginInstances] of pluginInstancesMap) {
-      for (const pluginInstance of pluginInstances) {
-        pluginInstance.checkDepExisted(pluginInstancesMap);
-        pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
-      }
+    for (const [_name, pluginInstance] of pluginInstanceMap) {
+      pluginInstance.checkDepExisted(pluginInstanceMap);
+      pluginDepEdgeList = pluginDepEdgeList.concat(pluginInstance.getDepEdgeList());
     }
-    const pluginSortResult: string[] = topologicalSort(pluginInstancesMap, pluginDepEdgeList);
-
-    if (pluginSortResult.length !== pluginInstancesMap.size) {
-      const diffPlugin = [...pluginInstancesMap.keys()].filter((name) => !pluginSortResult.includes(name));
+    const pluginSortResult: string[] = topologicalSort(pluginInstanceMap, pluginDepEdgeList);
+    if (pluginSortResult.length !== pluginInstanceMap.size) {
+      const diffPlugin = [...pluginInstanceMap.keys()].filter((name) => !pluginSortResult.includes(name));
       throw new Error(`There is a cycle in the dependencies, wrong plugin is ${diffPlugin.join(',')}.`);
     }
-
-    const result: BasePlugin[] = [];
-    for (const name of pluginSortResult) {
-      result.push(...(pluginInstancesMap.get(name) ?? []));
-    }
-    return this.filterDuplicatePlugins(result);
-  }
-
-  static async createFromConfig(config: Record<string, PluginConfigItem>): Promise<BasePlugin[]> {
-    const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
-    for (const [name, pluginConfig] of Object.entries(config)) {
-      pluginsConfig.set(name, [pluginConfig]);
-    }
-    return await this.createFromConfigList(pluginsConfig);
+    return pluginSortResult.map((name) => pluginInstanceMap.get(name)!);
   }
 
   static filterDuplicatePlugins(plugins: BasePlugin[]): BasePlugin[] {
diff --git a/src/plugin/types.ts b/src/plugin/types.ts
index 8ed846b..2399f8a 100644
--- a/src/plugin/types.ts
+++ b/src/plugin/types.ts
@@ -28,6 +28,6 @@ export interface Plugin {
   metaFilePath: string;
 
   init(): Promise<void>;
-  checkDepExisted(map: Map<string, Plugin[]>): void;
+  checkDepExisted(map: Map<string, Plugin>): void;
   getDepEdgeList(): [string, string][];
 }
diff --git a/src/scanner/scan.ts b/src/scanner/scan.ts
index f84061b..8d99b8e 100644
--- a/src/scanner/scan.ts
+++ b/src/scanner/scan.ts
@@ -21,11 +21,12 @@ import {
     ScannerOptions,
     WalkOptions,
     LoaderOptions,
+    ScanOptions,
 } from './types';
 import { Manifest, ManifestItem } from '../loader';
-import { BasePlugin, PluginFactory } from '../plugin';
+import ConfigurationHandler from '../configuration';
+import { PluginFactory } from '../plugin';
 import { FrameworkHandler } from '../framework';
-import { PluginConfigItem } from '../plugin/types';
 
 export class Scanner {
     private options: ScannerOptions;
@@ -54,29 +55,40 @@ export class Scanner {
         }
     }
 
-    public async scan(root: string) {
+    public async scan(root: string, options: ScanOptions = { env: ['default'] }): Promise<Record<string, Manifest>> {
+        const result = {};
+        for (const enviro of options.env) {
+            result[enviro] = await this.scanItems(root, enviro);
+        }
+        return result;
+    }
+
+    private async scanItems(root: string, env: string) {
         // 0. Scan Application
         await this.walk(root, { source: 'app', baseDir: root });
 
-        // 1. Calculate Plugin Load Order
-        const pluginsConfig: Map<string, PluginConfigItem[]> = new Map();
+        // 1. Scan Frameworks
+        await this.recurseFramework(env, root, []);
+
+        // 2. Calculate Plugin Load Order (need scan after framework because of plugin config maybe in framework)
+        const pluginConfigHandler = new ConfigurationHandler();
         for (const pluginConfigFile of this.itemMap.get('plugin-config') ?? []) {
-            const pluginConfig: Record<string, PluginConfigItem> = await compatibleRequire(pluginConfigFile.path);
-            for (const [name, config] of Object.entries(pluginConfig)) {
-                if (!config.path && !config.package) {
-                    continue;
-                }
-                const items = pluginsConfig.get(name);
-                if (Array.isArray(items)) {
-                    items.push(config);
-                    continue;
+            const pluginConfig = await compatibleRequire(pluginConfigFile.path);
+            if (pluginConfig) {
+                let [_, env, extname] = pluginConfigFile.filename.split('.');
+                if (!extname) {
+                    env = 'default';
                 }
-                pluginsConfig.set(name, [config]);
+                pluginConfigHandler.setConfig(env, pluginConfig);
             }
         }
-        const allPlugins: BasePlugin[] = await PluginFactory.createFromConfigList(pluginsConfig);
-        for (const plugin of allPlugins.reverse()) {
+        const mergedConfig = pluginConfigHandler.getMergedConfig(env);
+        const pluginSortedList = await PluginFactory.createFromConfig(mergedConfig || {});
+        for (const plugin of pluginSortedList.reverse()) {
             const metaList = this.itemMap.get('plugin-meta') ?? [];
+            if (!plugin.enable) {
+                continue;
+            }
             metaList.push({
                 path: plugin.metaFilePath,
                 extname: path.extname(plugin.metaFilePath),
@@ -88,42 +100,29 @@ export class Scanner {
             await this.walk(plugin.importPath, { source: 'plugin', baseDir: plugin.importPath, unitName: plugin.name });
         }
 
-        // 2. Scan Frameworks
-        const serialize = FrameworkHandler.serialize;
-        const frameworks: string[] = [];
-        const frameworkMap = new Map<string, boolean>();
-        frameworks.push(...serialize(this.itemMap.get('framework-config') ?? []));
-        frameworks.push(...serialize(this.itemMap.get('package-json') ?? []));
-        await this.recurseFramework(frameworks, root, frameworkMap);
-
         const result: Manifest = {
             items: this.getItemsFromMap(),
         };
         if (this.options.needWriteFile) {
-            this.writeFile('manifest.json', JSON.stringify(result, null, 2));
+            this.writeFile(`manifest.${env}.json`, JSON.stringify(result, null, 2));
         }
         return result;
     }
 
-    private async recurseFramework(frameworks: string[], frameworkBaseDir: string, frameworkMap: Map<string, boolean>) {
-        const serialize = FrameworkHandler.serialize;
-        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 && await this.walk(baseFrameworkPath, {
-                source: 'framework',
-                baseDir: baseFrameworkPath,
-                unitName: baseFrameworkPath
-            });
-            await this.recurseFramework([
-                ...serialize(this.itemMap.get('framework-config') ?? []),
-                ...serialize(this.itemMap.get('package-json') ?? [])
-            ], baseFrameworkPath, frameworkMap);
+    private async recurseFramework(env: string, frameworkBaseDir: string, executed: string[]) {
+        const { config, done } =
+            await FrameworkHandler.mergeConfig(env, this.itemMap.get('framework-config') ?? [], executed);
+        const baseFrameworkPath = await FrameworkHandler.handle(frameworkBaseDir, config);
+        if (!baseFrameworkPath) {
+            return;
         }
+        await this.walk(baseFrameworkPath, {
+            source: 'framework',
+            baseDir: baseFrameworkPath,
+            unitName: baseFrameworkPath
+        });
+
+        this.recurseFramework(env, baseFrameworkPath, executed.concat(done));
     }
 
     private async walk(
@@ -186,7 +185,10 @@ export class Scanner {
         for (const [, unitItems] of this.itemMap) {
             items = items.concat(unitItems);
         }
-        return items;
+        const apps = items.filter(item => item.source === 'app');
+        const plugins = items.filter(item => item.source === 'plugin');
+        const frameworks = items.filter(item => item.source === 'framework');
+        return frameworks.concat(plugins).concat(apps);
     }
 
     private isConfigDir(baseDir: string, currentDir: string): boolean {
diff --git a/src/scanner/types.ts b/src/scanner/types.ts
index e0570cf..42a8d4a 100644
--- a/src/scanner/types.ts
+++ b/src/scanner/types.ts
@@ -6,6 +6,10 @@ export interface ScannerOptions {
   conifgDir: string
 }
 
+export interface ScanOptions {
+  env: string[]
+}
+
 export interface WalkOptions {
   source: string,
   baseDir: string,
diff --git a/src/types.ts b/src/types.ts
index 1c28273..4cdeb13 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,7 +14,6 @@ export interface ApplicationLifecycle {
 
 export interface ApplicationInitOptions {
   containerName?: string;
-  envUnits?: string[];
 }
 
 export interface Application {


Test patch:
diff --git a/test/framework.test.ts b/test/framework.test.ts
index 63a8cc6..dc6600e 100644
--- a/test/framework.test.ts
+++ b/test/framework.test.ts
@@ -19,16 +19,14 @@ describe('test/framework.test.ts', () => {
       needWriteFile: false, extensions: ['.ts', '.js', '.json'],
       conifgDir: 'src/config'
     });
-    const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/artus-application'));
+    const { private: manifest } = await scanner.scan(path.resolve(__dirname, './fixtures/artus-application'), { env: ['private'] });
     // console.log('manifest', manifest);
     const { main } = await import('./fixtures/artus-application/src');
     const app = await main(manifest);
-    const { path: appFrameworkPath, choose: appFrameworkChoose } = app.artus.frameworks.get('app');
+    const { path: appFrameworkPath } = app.artus.frameworks.get('app');
     assert(appFrameworkPath);
-    assert(appFrameworkChoose === 'default');
-    const { path: barFrameworkPath, choose: barFrameworkChoose } = app.artus.frameworks.get(appFrameworkPath);
+    const { path: barFrameworkPath } = app.artus.frameworks.get(appFrameworkPath);
     assert(barFrameworkPath);
-    assert(barFrameworkChoose === 'private');
     assert(app.isListening());
     const port = app.artus.config?.port;
     assert(port === 3003);
diff --git a/test/scanner.test.ts b/test/scanner.test.ts
index 05fcde0..5204494 100644
--- a/test/scanner.test.ts
+++ b/test/scanner.test.ts
@@ -5,21 +5,21 @@ import path from 'path';
 describe('test/scanner.test.ts', () => {
     it('should scan application', async () => {
         const scanner = new Scanner({ needWriteFile: false, extensions: ['.ts', '.js', '.json'] });
-        const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app-koa-with-ts'));
+        const { default: manifest } = await scanner.scan(path.resolve(__dirname, './fixtures/app-koa-with-ts'));
         expect(manifest).toBeDefined();
         expect(manifest.items).toBeDefined();
         // console.log('manifest', manifest);
-        expect(manifest.items.length).toBe(14);
+        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(2);
+        expect(manifest.items.filter(item => item.loader === 'plugin-meta').length).toBe(1);
         expect(manifest.items.filter(item => item.loader === 'exception').length).toBe(1);
-        expect(manifest.items.filter(item => item.loader === 'extension').length).toBe(3);
+        expect(manifest.items.filter(item => item.loader === 'extension').length).toBe(2);
         expect(manifest.items.filter(item => item.loader === 'config').length).toBe(1);
         expect(manifest.items.filter(item => item.loader === 'module').length).toBe(5);
 
         expect(manifest.items.filter(item => item.unitName === 'redis').length).toBe(2);
-        expect(manifest.items.filter(item => item.unitName === 'mysql').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);
     });
 });