From 97987ba163a26cfb211d5279b11116257ac22c55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Ribeiro?= <jonnybgod@gmail.com>
Date: Thu, 5 Dec 2024 19:42:13 +0000
Subject: [PATCH] feat(core): adds ClsModule.registerPlugins to inject Plugins
 from an external module

---
 docs/docs/06_plugins/index.md            |  6 ++
 packages/core/src/lib/cls.module.ts      | 13 ++++
 packages/core/test/plugin/plugin.spec.ts | 76 ++++++++++++++++++++++--
 3 files changed, 90 insertions(+), 5 deletions(-)

diff --git a/docs/docs/06_plugins/index.md b/docs/docs/06_plugins/index.md
index 83cf6254..9ff1dcde 100644
--- a/docs/docs/06_plugins/index.md
+++ b/docs/docs/06_plugins/index.md
@@ -18,6 +18,12 @@ ClsModule.forRoot({
 });
 ```
 
+If you need to inject Plugins from an external module, use the `ClsModule.registerPlugins()` registration to import the containing module.
+
+```ts
+ClsModule.registerPlugins([new MyPlugin()]);
+```
+
 ## Available plugins
 
 For a list of plugins managed by the author of `nestjs-cls`, see the [Available Plugins](./01_available-plugins/index.md) page.
diff --git a/packages/core/src/lib/cls.module.ts b/packages/core/src/lib/cls.module.ts
index ed104a68..2008f0f5 100644
--- a/packages/core/src/lib/cls.module.ts
+++ b/packages/core/src/lib/cls.module.ts
@@ -40,6 +40,7 @@ import { ClsPluginManager } from './plugin/cls-plugin-manager';
 
 import { ProxyProviderManager } from './proxy-provider/proxy-provider-manager';
 import { ClsModuleProxyProviderOptions } from './proxy-provider/proxy-provider.interfaces';
+import { ClsPlugin } from './plugin/cls-plugin.interface';
 
 const clsServiceProvider: ValueProvider<ClsService> = {
     provide: ClsService,
@@ -192,6 +193,18 @@ export class ClsModule implements NestModule {
         };
     }
 
+    /**
+     * Registers the given Plugins the module along with `ClsService`.
+     */
+    static registerPlugins(plugins: ClsPlugin[]): DynamicModule {
+        return {
+            module: ClsModule,
+            imports: ClsPluginManager.registerPlugins(plugins),
+            providers: commonProviders,
+            exports: commonProviders,
+        };
+    }
+
     private static createProxyClassProviders(
         proxyProviderClasses?: Array<Type>,
     ) {
diff --git a/packages/core/test/plugin/plugin.spec.ts b/packages/core/test/plugin/plugin.spec.ts
index 9fab6115..b33e76c7 100644
--- a/packages/core/test/plugin/plugin.spec.ts
+++ b/packages/core/test/plugin/plugin.spec.ts
@@ -4,6 +4,14 @@ import { NestFactory } from '@nestjs/core';
 import { Controller, Get, Module } from '@nestjs/common';
 import supertest from 'supertest';
 
+function providerToken(name: string) {
+    return `${name}ProviderToken`;
+}
+
+function pluginInitializedToken(name: string) {
+    return `${name.toLocaleUpperCase()}_PLUGIN_INITIALIZED`;
+}
+
 function createDummyPlugin(name: string) {
     const watchers = {
         initHasRun: false,
@@ -18,11 +26,11 @@ function createDummyPlugin(name: string) {
             watchers.destroyHasRun = true;
         },
         onClsInit: (cls) => {
-            cls.set('PLUGIN_INITIALIZED', true);
+            cls.set(pluginInitializedToken(name), true);
         },
         providers: [
             {
-                provide: 'providerFromPlugin',
+                provide: providerToken(name),
                 useValue: 'valueFromPlugin',
             },
         ],
@@ -51,7 +59,7 @@ describe('Plugins', () => {
         await module.init();
 
         expect(watchers.initHasRun).toBe(true);
-        expect(module.get('providerFromPlugin')).toBe('valueFromPlugin');
+        expect(module.get(providerToken('forRoot'))).toBe('valueFromPlugin');
         expect(watchers.destroyHasRun).toBe(false);
 
         await module.close();
@@ -78,7 +86,9 @@ describe('Plugins', () => {
         await module.init();
 
         expect(watchers.initHasRun).toBe(true);
-        expect(module.get('providerFromPlugin')).toBe('valueFromPlugin');
+        expect(module.get(providerToken('forRootAsync'))).toBe(
+            'valueFromPlugin',
+        );
         expect(watchers.destroyHasRun).toBe(false);
 
         await module.close();
@@ -96,7 +106,7 @@ describe('Plugins', () => {
                 constructor(private readonly cls: ClsService) {}
                 @Get()
                 get() {
-                    return this.cls.get('PLUGIN_INITIALIZED');
+                    return this.cls.get(pluginInitializedToken('onClsInit'));
                 }
             }
 
@@ -123,4 +133,60 @@ describe('Plugins', () => {
             await module.close();
         },
     );
+
+    it('should register plugin and run module lifecycle and onClsInit methods (registerPlugins)', async () => {
+        const root = createDummyPlugin('root');
+        const feature = createDummyPlugin('feature');
+
+        @Controller()
+        class TestController {
+            constructor(private readonly cls: ClsService) {}
+            @Get()
+            get() {
+                return (
+                    this.cls.get(pluginInitializedToken('root')) &&
+                    this.cls.get(pluginInitializedToken('feature'))
+                );
+            }
+        }
+        @Module({
+            imports: [
+                ClsModule.forRoot({
+                    middleware: {
+                        mount: true,
+                    },
+                    plugins: [root.plugin],
+                }),
+                ClsModule.registerPlugins([feature.plugin]),
+            ],
+            controllers: [TestController],
+        })
+        class TestAppModule {}
+
+        const module = await NestFactory.create(TestAppModule);
+
+        expect(root.watchers.initHasRun).toBe(false);
+        expect(feature.watchers.initHasRun).toBe(false);
+
+        await module.init();
+
+        expect(root.watchers.initHasRun).toBe(true);
+        expect(feature.watchers.initHasRun).toBe(true);
+
+        expect(module.get(providerToken('root'))).toBe('valueFromPlugin');
+        expect(module.get(providerToken('feature'))).toBe('valueFromPlugin');
+
+        expect(root.watchers.destroyHasRun).toBe(false);
+        expect(feature.watchers.destroyHasRun).toBe(false);
+
+        await supertest(module.getHttpServer())
+            .get('/')
+            .expect(200)
+            .expect('true');
+
+        await module.close();
+
+        expect(root.watchers.destroyHasRun).toBe(true);
+        expect(feature.watchers.destroyHasRun).toBe(true);
+    });
 });