Repository: artusjs/core
Pull request #272 (2024-01-24 11:41:45)

fix: merge plugin config when load because of plugin preset
之前在 Scan 阶段预处理的 pluginConfig，存在边界 case：

- 插件合集 a 内 default 启用了插件 b
- app 在 prod 环境未启用插件合集 a
- 但预处理过程中把全部 default 合并，全部 prod 合并
- 预期应为 prod 是 a 禁用，所以不涉及 b 的工作
- 但实际出现总和 default 中 b 启用，prod 中 a 禁用，合并结果中 b 仍被启用

本 PR 调整了 Scanner & Loader 的插件配置部分逻辑：

- 将 pluginConfig 字段分拆到 refMap 和 extraPluginConfig（扫描时指定的）；
- load 阶段以 ref（插件/应用）为单位，每层按环境合并插件配置；
- 用上面按环境合并的配置排序、加载 itemList


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs) Report
Attention: `3 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`a2e5f6d`)](https://app.codecov.io/gh/artusjs/core/commit/a2e5f6de77f0dfea0faaa014acfa4c2f7f0b9763?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs) 91.38% compared to head [(`7c664d1`)](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs) 91.34%.

| [Files](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs) | Patch % | Lines |
|---|---|---|
| [src/configuration/index.ts](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs#diff-c3JjL2NvbmZpZ3VyYXRpb24vaW5kZXgudHM=) | 57.14% | [3 Missing :warning: ](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #272      +/-   ##
==========================================
- Coverage   91.38%   91.34%   -0.05%     
==========================================
  Files          49       49              
  Lines        1196     1202       +6     
  Branches      218      218              
==========================================
+ Hits         1093     1098       +5     
- Misses        103      104       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/artusjs/core/pull/272?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=artusjs).


Patch:
diff --git a/.gitignore b/.gitignore
index 9a2556f..70a1b50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -115,5 +115,3 @@ lib
 
 # OSX
 .DS_Store
-
-!test/fixtures/frameworks/layer/foo/foo2/node_modules
\ No newline at end of file
diff --git a/src/configuration/index.ts b/src/configuration/index.ts
index eb04a8c..a59c373 100644
--- a/src/configuration/index.ts
+++ b/src/configuration/index.ts
@@ -5,9 +5,7 @@ import { mergeConfig } from '../loader/utils/merge';
 import compatibleRequire from '../utils/compatible_require';
 
 export type ConfigObject = Record<string, any>;
-export type FrameworkObject = { path: string; env: string };
 export type PackageObject = ConfigObject;
-export type FrameworkOptions = { env: string; unitName: string };
 
 @Injectable()
 export default class ConfigurationHandler {
@@ -19,28 +17,32 @@ export default class ConfigurationHandler {
     return env;
   }
 
-  public configStore: Map<string, ConfigObject> = new Map();
+  public configStore: Record<string, ConfigObject> = {};
 
   @Inject()
   private container: Container;
 
   getMergedConfig(): ConfigObject {
+    return this.mergeConfigByStore(this.configStore);
+  }
+
+  mergeConfigByStore(store: Record<string, ConfigObject>): ConfigObject {
     let envList: string[] = this.container.get(ArtusInjectEnum.EnvList, { noThrow: true });
     if (!envList) {
       envList = process.env[ARTUS_SERVER_ENV] ? [process.env[ARTUS_SERVER_ENV]] : [ARTUS_DEFAULT_CONFIG_ENV.DEV];
     }
-    const defaultConfig = this.configStore.get(ARTUS_DEFAULT_CONFIG_ENV.DEFAULT) ?? {};
-    const envConfigList = envList.map(currentEnv => (this.configStore.get(currentEnv) ?? {}));
+    const defaultConfig = store[ARTUS_DEFAULT_CONFIG_ENV.DEFAULT] ?? {};
+    const envConfigList = envList.map(currentEnv => (store[currentEnv] ?? {}));
     return mergeConfig(defaultConfig, ...envConfigList);
   }
 
   clearStore(): void {
-    this.configStore.clear();
+    this.configStore = {};
   }
 
   setConfig(env: string, config: ConfigObject) {
-    const storedConfig = this.configStore.get(env) ?? {};
-    this.configStore.set(env, mergeConfig(storedConfig, config));
+    const storedConfig = this.configStore[env] ?? {};
+    this.configStore[env] = mergeConfig(storedConfig, config);
   }
 
   async setConfigByFile(fileItem: ManifestItem) {
diff --git a/src/constant.ts b/src/constant.ts
index be29418..15f9558 100644
--- a/src/constant.ts
+++ b/src/constant.ts
@@ -53,7 +53,6 @@ export const DEFAULT_LOADER_LIST_WITH_ORDER = [
   'exception',
   'exception-filter',
   'plugin-meta',
-  'framework-config',
   'package-json',
   'module',
   'lifecycle-hook-unit',
diff --git a/src/loader/factory.ts b/src/loader/factory.ts
index 2d55167..d04635f 100644
--- a/src/loader/factory.ts
+++ b/src/loader/factory.ts
@@ -1,6 +1,6 @@
 import * as path from 'path';
 import { Container, Injectable, Inject, ScopeEnum } from '@artus/injection';
-import { DEFAULT_LOADER, LOADER_NAME_META, DEFAULT_LOADER_LIST_WITH_ORDER, DEFAULT_APP_REF } from '../constant';
+import { DEFAULT_LOADER, LOADER_NAME_META, DEFAULT_LOADER_LIST_WITH_ORDER, DEFAULT_APP_REF, ARTUS_DEFAULT_CONFIG_ENV } from '../constant';
 import {
   Manifest,
   ManifestItem,
@@ -10,8 +10,9 @@ import {
 import ConfigurationHandler from '../configuration';
 import { LifecycleManager } from '../lifecycle';
 import LoaderEventEmitter, { LoaderEventListener } from './loader_event';
-import { PluginConfigItem, PluginFactory } from '../plugin';
+import { PluginConfig, PluginFactory } from '../plugin';
 import { Logger, LoggerType } from '../logger';
+import { mergeConfig } from './utils/merge';
 
 @Injectable({
   scope: ScopeEnum.SINGLETON,
@@ -69,15 +70,20 @@ export class LoaderFactory {
     // Manifest Version 2 is supported mainly
 
     // Merge plugin config with ref
-    for (const [env, pluginConfig] of Object.entries(manifest.pluginConfig ?? {})) {
-      this.configurationHandler.setConfig(env, {
-        plugin: pluginConfig,
-      });
-    }
-    const mergedPluginConfig: Record<string, PluginConfigItem> = Object.assign(
-      {},
-      this.configurationHandler.getMergedConfig()?.plugin ?? {},
-    ); // shallow copy to avoid side effect of writing metadata
+    const mergeRef = (refName: string) => {
+      if (!refName || !manifest.refMap?.[refName]) {
+        return {};
+      }
+      const pluginConfig = this.configurationHandler.mergeConfigByStore(manifest.refMap[refName].pluginConfig ?? {});
+      return mergeConfig(...Object.values(pluginConfig).map(({ refName }) => mergeRef(refName)).concat(pluginConfig));
+    };
+    const mergedPluginConfig = mergeConfig(manifest.extraPluginConfig ?? {}, ...[
+      ...Object.values(manifest.extraPluginConfig ?? {}).map(({ refName }) => refName),
+      DEFAULT_APP_REF,
+    ].map(mergeRef)) as PluginConfig;
+    this.configurationHandler.setConfig(ARTUS_DEFAULT_CONFIG_ENV.DEFAULT, {
+      plugin: mergedPluginConfig,
+    }); // For compatible
     for (const [pluginName, pluginConfigItem] of Object.entries(mergedPluginConfig)) {
       const refItem = manifest.refMap[pluginConfigItem.refName];
       if (!refItem) {
diff --git a/src/loader/impl/config.ts b/src/loader/impl/config.ts
index ae3560f..53c9184 100644
--- a/src/loader/impl/config.ts
+++ b/src/loader/impl/config.ts
@@ -41,11 +41,6 @@ class ConfigLoader implements Loader {
   async load(item: ManifestItem) {
     const { namespace, env } = getConfigMetaFromFilename(item.filename);
     let configObj = await this.loadConfigFile(item);
-    // if (namespace === 'plugin') {
-    // configObj = {
-    //  plugin: await PluginFactory.formatPluginConfig(configObj, item),
-    // };
-    // } else
     if (namespace) {
       configObj = {
         [namespace]: configObj,
diff --git a/src/loader/types.ts b/src/loader/types.ts
index a2c36d1..df05672 100644
--- a/src/loader/types.ts
+++ b/src/loader/types.ts
@@ -9,6 +9,7 @@ export interface RefMapItem {
   relativedPath?: string;
   packageVersion?: string;
   pluginMetadata?: PluginMetadata;
+  pluginConfig: PluginConfigEnvMap;
   items: ManifestItem[];
 }
 // Key: RefName => RefMapItem
@@ -16,8 +17,8 @@ export type RefMap = Record<string, RefMapItem>;
 
 export interface Manifest {
   version: '2';
-  pluginConfig: PluginConfigEnvMap;
   refMap: RefMap;
+  extraPluginConfig?: PluginConfig;
 }
 
 export interface ManifestItem<LoaderState = unknown> extends Record<string, any> {
diff --git a/src/scanner/task.ts b/src/scanner/task.ts
index d94138e..3eacf67 100644
--- a/src/scanner/task.ts
+++ b/src/scanner/task.ts
@@ -2,18 +2,17 @@ import path from 'path';
 import * as fs from 'fs/promises';
 import { ScannerOptions, ScanTaskItem, WalkOptions } from './types';
 import { existsAsync, getPackageVersion, isExclude, isPluginAsync, loadConfigItemList, resolvePluginConfigItemRef } from './utils';
-import { findLoader, Manifest, ManifestItem, PluginConfigEnvMap, RefMap, RefMapItem } from '../loader';
+import { findLoader, Manifest, ManifestItem, RefMap, RefMapItem } from '../loader';
 import { PluginConfig, PluginMetadata } from '../plugin';
-import { mergeConfig } from '../loader/utils/merge';
 import { loadMetaFile } from '../utils/load_meta_file';
-import { ARTUS_DEFAULT_CONFIG_ENV, DEFAULT_APP_REF, PLUGIN_META_FILENAME } from '../constant';
+import { DEFAULT_APP_REF, PLUGIN_META_FILENAME } from '../constant';
 import { Application } from '../types';
 import { ArtusApplication } from '../application';
 
 export class ScanTaskRunner {
   private waitingTaskMap: Map<string, ScanTaskItem[]> = new Map(); // Key is pluginName, waiting to detect enabled
   private enabledPluginSet: Set<string> = new Set(); // Key is pluginName
-  private pluginConfigMap: PluginConfigEnvMap = {};
+  private extraPluginConfig: PluginConfig = {};
   private refMap: RefMap = {};
   private taskQueue: ScanTaskItem[] = [];
   private app: Application;
@@ -99,14 +98,13 @@ export class ScanTaskRunner {
   public async handlePluginConfig(
     pluginConfig: PluginConfig,
     basePath: string,
-    env: string = ARTUS_DEFAULT_CONFIG_ENV.DEFAULT,
-  ): Promise<void> {
-    const tPluginConfig: PluginConfig = {};
+  ): Promise<PluginConfig> {
+    const res: PluginConfig = {};
     for (const [pluginName, pluginConfigItem] of Object.entries(pluginConfig)) {
       // Set temp pluginConfig in manifest
-      tPluginConfig[pluginName] = {};
+      res[pluginName] = {};
       if (pluginConfigItem.enable !== undefined) {
-        tPluginConfig[pluginName].enable = pluginConfigItem.enable;
+        res[pluginName].enable = pluginConfigItem.enable;
       }
       if (pluginConfigItem.enable) {
         this.enabledPluginSet.add(pluginName);
@@ -118,7 +116,7 @@ export class ScanTaskRunner {
       if (!ref?.name) {
         continue;
       }
-      tPluginConfig[pluginName].refName = ref.name;
+      res[pluginName].refName = ref.name;
       // Generate and push scan task
       const curRefTask: ScanTaskItem = {
         curPath: ref.path,
@@ -140,9 +138,7 @@ export class ScanTaskRunner {
         this.waitingTaskMap.set(pluginName, waitingTaskList);
       }
     }
-    // Reverse Merge, The prior of top-level(exists) is higher
-    const existsPluginConfig = this.pluginConfigMap[env] ?? {};
-    this.pluginConfigMap[env] = mergeConfig(tPluginConfig, existsPluginConfig) as PluginConfig;
+    return res;
   }
 
 
@@ -195,6 +191,7 @@ export class ScanTaskRunner {
     const refItem: RefMapItem = {
       relativedPath,
       packageVersion,
+      pluginConfig: {},
       items: [],
     };
 
@@ -217,7 +214,7 @@ export class ScanTaskRunner {
       if (!pluginConfig) {
         continue;
       }
-      await this.handlePluginConfig(pluginConfig, basePath, env);
+      refItem.pluginConfig[env] = await this.handlePluginConfig(pluginConfig, basePath);
     }
 
     if (this.options.useRelativePath) {
@@ -233,7 +230,7 @@ export class ScanTaskRunner {
   public async runAll(): Promise<void> {
     // Add Task of options.plugin
     if (this.options.plugin) {
-      await this.handlePluginConfig(this.options.plugin, this.root);
+      this.extraPluginConfig = await this.handlePluginConfig(this.options.plugin, this.root);
     }
 
     // Add Root Task(make it as top/start)
@@ -253,8 +250,8 @@ export class ScanTaskRunner {
   public dump(): Manifest {
     return {
       version: '2',
-      pluginConfig: this.pluginConfigMap,
       refMap: this.refMap,
+      extraPluginConfig: this.extraPluginConfig,
     };
   }
 }
diff --git a/src/scanner/utils.ts b/src/scanner/utils.ts
index bf011d0..48e6bb3 100644
--- a/src/scanner/utils.ts
+++ b/src/scanner/utils.ts
@@ -52,7 +52,7 @@ export const loadConfigItemList = async <T = ConfigObject>(configItemList: Manif
   }
 
   // Use temp Map to store config
-  const configEnvMap: Map<string, T> = new Map();
+  const configEnvMap: Record<string, T> = {};
   const stashedConfigStore = app.configurationHandler.configStore;
   app.configurationHandler.configStore = configEnvMap;
 
@@ -65,7 +65,7 @@ export const loadConfigItemList = async <T = ConfigObject>(configItemList: Manif
   // Restore config store
   app.configurationHandler.configStore = stashedConfigStore;
 
-  return Object.fromEntries(configEnvMap.entries());
+  return configEnvMap;
 };
 
 export const resolvePluginConfigItemRef = async (


Test patch:
diff --git a/test/__snapshots__/scanner.test.ts.snap b/test/__snapshots__/scanner.test.ts.snap
index 87519b8..05389f8 100644
--- a/test/__snapshots__/scanner.test.ts.snap
+++ b/test/__snapshots__/scanner.test.ts.snap
@@ -2,32 +2,12 @@
 
 exports[`test/scanner.test.ts should be scan application 1`] = `
 {
-  "pluginConfig": {
-    "default": {
-      "mysql": {
-        "enable": false,
-        "refName": "src/mysql_plugin",
-      },
-      "redis": {
-        "enable": true,
-        "refName": "src/redis_plugin",
-      },
-      "testDuplicate": {
-        "enable": false,
-        "refName": "@artus/injection",
-      },
-    },
-    "dev": {
-      "testDuplicate": {
-        "enable": true,
-        "refName": "src/test_duplicate_plugin",
-      },
-    },
-  },
+  "extraPluginConfig": {},
   "refMap": {
     "@artus/injection": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "relativedPath": "../../../node_modules/@artus/injection/lib",
     },
     "_app": {
@@ -154,6 +134,28 @@ exports[`test/scanner.test.ts should be scan application 1`] = `
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "mysql": {
+            "enable": false,
+            "refName": "src/mysql_plugin",
+          },
+          "redis": {
+            "enable": true,
+            "refName": "src/redis_plugin",
+          },
+          "testDuplicate": {
+            "enable": false,
+            "refName": "@artus/injection",
+          },
+        },
+        "dev": {
+          "testDuplicate": {
+            "enable": true,
+            "refName": "src/test_duplicate_plugin",
+          },
+        },
+      },
       "relativedPath": "",
     },
     "src/redis_plugin": {
@@ -173,6 +175,7 @@ exports[`test/scanner.test.ts should be scan application 1`] = `
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "exclude": [
           "not_to_be_scanned_dir",
@@ -185,6 +188,7 @@ exports[`test/scanner.test.ts should be scan application 1`] = `
     "src/test_duplicate_plugin": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "name": "testDuplicate",
       },
@@ -197,54 +201,17 @@ exports[`test/scanner.test.ts should be scan application 1`] = `
 
 exports[`test/scanner.test.ts should scan application with nesting preset a which defined in options 1`] = `
 {
-  "pluginConfig": {
-    "default": {
-      "a": {
-        "enable": false,
-        "refName": "../plugins/plugin_a",
-      },
-      "b": {
-        "enable": false,
-        "refName": "../plugins/plugin_b",
-      },
-      "c": {
-        "enable": false,
-        "refName": "../plugins/plugin_c",
-      },
-      "d": {
-        "enable": true,
-        "refName": "../plugins/plugin_d",
-      },
-      "plugin-with-entry-a": {
-        "enable": true,
-        "refName": "../plugins/plugin_with_entry_a",
-      },
-      "plugin-with-entry-b": {
-        "enable": true,
-        "refName": "../plugins/plugin_with_entry_b",
-      },
-      "plugin-with-entry-c": {
-        "enable": false,
-        "refName": "../plugins/plugin_with_entry_c",
-      },
-      "preset_a": {
-        "enable": true,
-        "refName": "../plugins/preset_a",
-      },
-      "preset_b": {
-        "enable": true,
-        "refName": "../plugins/preset_b",
-      },
-      "preset_c": {
-        "enable": true,
-        "refName": "../plugins/preset_c",
-      },
+  "extraPluginConfig": {
+    "preset_a": {
+      "enable": true,
+      "refName": "../plugins/preset_a",
     },
   },
   "refMap": {
     "../plugins/plugin_a": {
       "items": [],
       "packageVersion": "0.0.1",
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -262,6 +229,7 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
     "../plugins/plugin_b": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -275,6 +243,7 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
     "../plugins/plugin_d": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -289,6 +258,7 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
     "../plugins/plugin_with_entry_a": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "name": "plugin-with-entry-a",
       },
@@ -297,6 +267,7 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
     "../plugins/plugin_with_entry_b": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "name": "plugin-with-entry-b",
       },
@@ -317,6 +288,25 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "a": {
+            "enable": false,
+          },
+          "plugin-with-entry-a": {
+            "enable": true,
+            "refName": "../plugins/plugin_with_entry_a",
+          },
+          "preset_b": {
+            "enable": true,
+            "refName": "../plugins/preset_b",
+          },
+          "preset_c": {
+            "enable": true,
+            "refName": "../plugins/preset_c",
+          },
+        },
+      },
       "pluginMetadata": {
         "name": "preset_a",
       },
@@ -337,6 +327,22 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "a": {
+            "enable": true,
+            "refName": "../plugins/plugin_a",
+          },
+          "b": {
+            "enable": true,
+            "refName": "../plugins/plugin_b",
+          },
+          "plugin-with-entry-b": {
+            "enable": true,
+            "refName": "../plugins/plugin_with_entry_b",
+          },
+        },
+      },
       "pluginMetadata": {
         "name": "preset_b",
       },
@@ -357,6 +363,26 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "b": {
+            "enable": false,
+            "refName": "../plugins/plugin_b",
+          },
+          "c": {
+            "enable": false,
+            "refName": "../plugins/plugin_c",
+          },
+          "d": {
+            "enable": true,
+            "refName": "../plugins/plugin_d",
+          },
+          "plugin-with-entry-c": {
+            "enable": false,
+            "refName": "../plugins/plugin_with_entry_c",
+          },
+        },
+      },
       "pluginMetadata": {
         "name": "preset_c",
       },
@@ -365,6 +391,7 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
     "_app": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "relativedPath": "",
     },
   },
@@ -374,30 +401,12 @@ exports[`test/scanner.test.ts should scan application with nesting preset a whic
 
 exports[`test/scanner.test.ts should scan application with single preset b which defined in config 1`] = `
 {
-  "pluginConfig": {
-    "default": {
-      "a": {
-        "enable": true,
-        "refName": "../plugins/plugin_a",
-      },
-      "b": {
-        "enable": true,
-        "refName": "../plugins/plugin_b",
-      },
-      "plugin-with-entry-b": {
-        "enable": true,
-        "refName": "../plugins/plugin_with_entry_b",
-      },
-      "preset_b": {
-        "enable": true,
-        "refName": "../plugins/preset_b",
-      },
-    },
-  },
+  "extraPluginConfig": {},
   "refMap": {
     "../plugins/plugin_a": {
       "items": [],
       "packageVersion": "0.0.1",
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -415,6 +424,7 @@ exports[`test/scanner.test.ts should scan application with single preset b which
     "../plugins/plugin_b": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -428,6 +438,7 @@ exports[`test/scanner.test.ts should scan application with single preset b which
     "../plugins/plugin_with_entry_b": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "name": "plugin-with-entry-b",
       },
@@ -448,6 +459,22 @@ exports[`test/scanner.test.ts should scan application with single preset b which
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "a": {
+            "enable": true,
+            "refName": "../plugins/plugin_a",
+          },
+          "b": {
+            "enable": true,
+            "refName": "../plugins/plugin_b",
+          },
+          "plugin-with-entry-b": {
+            "enable": true,
+            "refName": "../plugins/plugin_with_entry_b",
+          },
+        },
+      },
       "pluginMetadata": {
         "name": "preset_b",
       },
@@ -468,6 +495,14 @@ exports[`test/scanner.test.ts should scan application with single preset b which
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "preset_b": {
+            "enable": true,
+            "refName": "../plugins/preset_b",
+          },
+        },
+      },
       "relativedPath": "",
     },
   },
@@ -477,34 +512,17 @@ exports[`test/scanner.test.ts should scan application with single preset b which
 
 exports[`test/scanner.test.ts should scan application with single preset c which defined in options 1`] = `
 {
-  "pluginConfig": {
-    "default": {
-      "b": {
-        "enable": false,
-        "refName": "../plugins/plugin_b",
-      },
-      "c": {
-        "enable": false,
-        "refName": "../plugins/plugin_c",
-      },
-      "d": {
-        "enable": true,
-        "refName": "../plugins/plugin_d",
-      },
-      "plugin-with-entry-c": {
-        "enable": false,
-        "refName": "../plugins/plugin_with_entry_c",
-      },
-      "preset_c": {
-        "enable": true,
-        "refName": "../plugins/preset_c",
-      },
+  "extraPluginConfig": {
+    "preset_c": {
+      "enable": true,
+      "refName": "../plugins/preset_c",
     },
   },
   "refMap": {
     "../plugins/plugin_d": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "pluginMetadata": {
         "dependencies": [
           {
@@ -531,6 +549,26 @@ exports[`test/scanner.test.ts should scan application with single preset c which
         },
       ],
       "packageVersion": undefined,
+      "pluginConfig": {
+        "default": {
+          "b": {
+            "enable": false,
+            "refName": "../plugins/plugin_b",
+          },
+          "c": {
+            "enable": false,
+            "refName": "../plugins/plugin_c",
+          },
+          "d": {
+            "enable": true,
+            "refName": "../plugins/plugin_d",
+          },
+          "plugin-with-entry-c": {
+            "enable": false,
+            "refName": "../plugins/plugin_with_entry_c",
+          },
+        },
+      },
       "pluginMetadata": {
         "name": "preset_c",
       },
@@ -539,6 +577,7 @@ exports[`test/scanner.test.ts should scan application with single preset c which
     "_app": {
       "items": [],
       "packageVersion": undefined,
+      "pluginConfig": {},
       "relativedPath": "",
     },
   },
diff --git a/test/config.test.ts b/test/config.test.ts
index f75f9f6..e4150a9 100644
--- a/test/config.test.ts
+++ b/test/config.test.ts
@@ -9,6 +9,7 @@ describe("test/config.test.ts", () => {
       const app = await main();
       expect(app.config).toEqual({
         name: "test-for-config",
+        plugin: {},
         test: 1,
         arr: [4, 5, 6],
       });
diff --git a/test/fixtures/app_koa_with_ts/src/bootstrap.ts b/test/fixtures/app_koa_with_ts/src/bootstrap.ts
index 0ee5ce3..a16359d 100644
--- a/test/fixtures/app_koa_with_ts/src/bootstrap.ts
+++ b/test/fixtures/app_koa_with_ts/src/bootstrap.ts
@@ -8,9 +8,9 @@ export const app: ArtusApplication = new ArtusApplication();
 async function main() {
   await app.load({
     version: '2',
-    pluginConfig: {},
     refMap: {
       _app: {
+        pluginConfig: {},
         items: [
           {
             path: path.resolve(__dirname, "./lifecycle"),
diff --git a/test/fixtures/app_with_config/bootstrap.ts b/test/fixtures/app_with_config/bootstrap.ts
index fa5932c..55d4615 100644
--- a/test/fixtures/app_with_config/bootstrap.ts
+++ b/test/fixtures/app_with_config/bootstrap.ts
@@ -7,9 +7,9 @@ async function main() {
   const app = new ArtusApplication();
   await app.load({
     version: "2",
-    pluginConfig: {},
     refMap: {
       _app: {
+        pluginConfig: {},
         items: [
           {
             path: path.resolve(__dirname, "./app"),
diff --git a/test/fixtures/exception_filter/bootstrap.ts b/test/fixtures/exception_filter/bootstrap.ts
index 739d34f..cf0ebf6 100644
--- a/test/fixtures/exception_filter/bootstrap.ts
+++ b/test/fixtures/exception_filter/bootstrap.ts
@@ -9,9 +9,9 @@ async function main() {
   });
   await app.load({
     version: "2",
-    pluginConfig: {},
     refMap: {
       _app: {
+        pluginConfig: {},
         items: [
           {
             path: path.resolve(__dirname, "./filter"),
diff --git a/test/fixtures/exception_invalid_filter/bootstrap.ts b/test/fixtures/exception_invalid_filter/bootstrap.ts
index 1dc6cb4..11756c0 100644
--- a/test/fixtures/exception_invalid_filter/bootstrap.ts
+++ b/test/fixtures/exception_invalid_filter/bootstrap.ts
@@ -9,9 +9,9 @@ async function main() {
   });
   await app.load({
     version: "2",
-    pluginConfig: {},
     refMap: {
       _app: {
+        pluginConfig: {},
         items: [
           {
             path: path.resolve(__dirname, "./filter"),
diff --git a/test/fixtures/exception_with_ts_yaml/bootstrap.ts b/test/fixtures/exception_with_ts_yaml/bootstrap.ts
index 278e9ac..f0d39c9 100644
--- a/test/fixtures/exception_with_ts_yaml/bootstrap.ts
+++ b/test/fixtures/exception_with_ts_yaml/bootstrap.ts
@@ -6,9 +6,9 @@ async function main() {
   const app = new ArtusApplication();
   await app.load({
     version: "2",
-    pluginConfig: {},
     refMap: {
       _app: {
+        pluginConfig: {},
         items: [
           {
             path: path.resolve(__dirname, "./app"),
diff --git a/test/fixtures/logger/src/index.ts b/test/fixtures/logger/src/index.ts
index aeec49d..c3962f9 100644
--- a/test/fixtures/logger/src/index.ts
+++ b/test/fixtures/logger/src/index.ts
@@ -5,9 +5,9 @@ const rootDir = path.resolve(__dirname, "./");
 
 const defaultManifest: Manifest = {
   version: "2",
-  pluginConfig: {},
   refMap: {
     _app: {
+      pluginConfig: {},
       items: [
         {
           path: path.resolve(rootDir, "./test_clazz"),
@@ -21,9 +21,9 @@ const defaultManifest: Manifest = {
 
 export const manifestWithCustomLogger: Manifest = {
   version: "2",
-  pluginConfig: {},
   refMap: {
     _app: {
+      pluginConfig: {},
       items: [
         ...defaultManifest.refMap._app.items,
         {
diff --git a/test/fixtures/module_with_custom_loader/src/index.ts b/test/fixtures/module_with_custom_loader/src/index.ts
index 306f0af..aef4763 100644
--- a/test/fixtures/module_with_custom_loader/src/index.ts
+++ b/test/fixtures/module_with_custom_loader/src/index.ts
@@ -5,9 +5,9 @@ const rootDir = path.resolve(__dirname, "./");
 
 export default {
   version: "2",
-  pluginConfig: {},
   refMap: {
     _app: {
+      pluginConfig: {},
       items: [
         {
           path: path.resolve(rootDir, "./test_clazz"),
diff --git a/test/fixtures/module_with_ts/src/index.ts b/test/fixtures/module_with_ts/src/index.ts
index 09687ea..089e2d0 100644
--- a/test/fixtures/module_with_ts/src/index.ts
+++ b/test/fixtures/module_with_ts/src/index.ts
@@ -5,9 +5,9 @@ const rootDir = path.resolve(__dirname, "./");
 
 export default {
   version: "2",
-  pluginConfig: {},
   refMap: {
     _app: {
+      pluginConfig: {},
       items: [
         {
           path: path.resolve(rootDir, "./test_service_a"),
diff --git a/test/utils/index.ts b/test/utils/index.ts
index 72b9af2..c3897bc 100644
--- a/test/utils/index.ts
+++ b/test/utils/index.ts
@@ -1,10 +1,9 @@
 import 'reflect-metadata';
 import os from 'os';
-import { ArtusScanner, ArtusApplication, Manifest, RefMap } from '../../src';
+import { ArtusScanner, ArtusApplication, Manifest, RefMap, PluginConfig } from '../../src';
 
 export const DEFAULT_EMPTY_MANIFEST: Manifest = {
   version: '2',
-  pluginConfig: {},
   refMap: {},
 };
 
@@ -29,16 +28,24 @@ export function formatManifestForWindowsTest(manifest: Manifest) {
   }
   // A regexp for convert win32 path delimiter to POSIX style
   const pathReg = /\\/g;
-  for (const pluginConfig of Object.values(manifest.pluginConfig)) {
-    for (const pluginConfigItem of Object.values(pluginConfig)) {
-      if (!pluginConfigItem.refName) {
-        continue;
-      }
-      pluginConfigItem.refName = pluginConfigItem.refName.replace(pathReg, '/');
-    }
-  }
   const newRefMap: RefMap = {};
+  const handlePluginConfig = (pluginConfig: PluginConfig) => {
+    return Object.fromEntries(Object.entries(pluginConfig).map(([pluginName, pluginConfigItem]) => {
+      if (pluginConfigItem.refName) {
+        pluginConfigItem.refName = pluginConfigItem.refName.replace(pathReg, '/');
+      }
+      return [pluginName, pluginConfigItem];
+    }));
+  };
   for (const [refName, refItem] of Object.entries(manifest.refMap)) {
+    for (const pluginConfig of Object.values(refItem.pluginConfig)) {
+      for (const pluginConfigItem of Object.values(pluginConfig)) {
+        if (!pluginConfigItem.refName) {
+          continue;
+        }
+        pluginConfigItem.refName = pluginConfigItem.refName.replace(pathReg, '/');
+      }
+    }
     newRefMap[refName.replace(pathReg, '/')] = {
       ...refItem,
       relativedPath: refItem.relativedPath.replace(pathReg, '/'),
@@ -46,8 +53,12 @@ export function formatManifestForWindowsTest(manifest: Manifest) {
         ...item,
         path: item.path.replace(pathReg, '/'),
       })),
+      pluginConfig: Object.fromEntries(Object.entries(refItem.pluginConfig).map(
+        ([env, pluginConfig]) => [env, handlePluginConfig(pluginConfig)],
+      )),
     };
   }
   manifest.refMap = newRefMap;
+  manifest.extraPluginConfig = handlePluginConfig(manifest.extraPluginConfig);
   return manifest;
 }