├── test ├── __fixture__ │ ├── group-one │ │ ├── excluded │ │ │ └── dino.wild.ts │ │ ├── lion.wild.ts │ │ └── hippo.wild.ts │ ├── filter │ │ ├── command.constant.ts │ │ └── play.command.ts │ ├── group-two │ │ ├── cat.pet.ts │ │ └── dog.pet.ts │ ├── not-for-search │ │ └── veterinarian.ts │ └── index.ts └── decorator.spec.ts ├── .prettierrc ├── src ├── definition │ ├── pattern.ts │ ├── guard │ │ ├── is-pattern.ts │ │ └── is-dynamic-provider-options.ts │ ├── store-value.ts │ ├── resolved-type-providers.ts │ └── dynamic-provider-options.ts ├── utils │ └── function │ │ └── is-object.ts ├── index.ts ├── service │ └── resolve-file.service.ts └── decorator │ └── inject-dynamic-providers.ts ├── tsup.config.ts ├── tsconfig.json ├── .gitignore ├── eslint.config.js ├── .github └── workflows │ └── test.yml ├── package.json └── README.md /test/__fixture__/group-one/excluded/dino.wild.ts: -------------------------------------------------------------------------------- 1 | export class Dino {} 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/definition/pattern.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Glob pattern. 3 | */ 4 | export type Pattern = string; 5 | -------------------------------------------------------------------------------- /test/__fixture__/filter/command.constant.ts: -------------------------------------------------------------------------------- 1 | export const COMMAND_METADATA = '__command_metadata__'; 2 | -------------------------------------------------------------------------------- /test/__fixture__/group-two/cat.pet.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class Cat {} 5 | -------------------------------------------------------------------------------- /test/__fixture__/group-two/dog.pet.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class Dog {} 5 | -------------------------------------------------------------------------------- /test/__fixture__/group-one/lion.wild.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class Lion {} 5 | -------------------------------------------------------------------------------- /test/__fixture__/not-for-search/veterinarian.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class Veterinarian {} 5 | -------------------------------------------------------------------------------- /test/__fixture__/group-one/hippo.wild.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | export const HIPPO_CONST = 'HIPPO_CONST'; 4 | 5 | @Injectable() 6 | export class Hippo {} 7 | -------------------------------------------------------------------------------- /src/definition/guard/is-pattern.ts: -------------------------------------------------------------------------------- 1 | import { Pattern } from '../pattern'; 2 | 3 | export function isPattern(param: any[]): param is Pattern[] { 4 | return typeof param[0] === 'string'; 5 | } 6 | -------------------------------------------------------------------------------- /src/definition/store-value.ts: -------------------------------------------------------------------------------- 1 | import { DynamicProviderOptions } from './dynamic-provider-options'; 2 | 3 | export interface StoreValue { 4 | target: Function; 5 | options: DynamicProviderOptions[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/function/is-object.ts: -------------------------------------------------------------------------------- 1 | export function IsObject(instance: any): instance is object { 2 | return typeof instance === 'object' 3 | ? instance !== null 4 | : typeof instance === 'function'; 5 | } 6 | -------------------------------------------------------------------------------- /src/definition/resolved-type-providers.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { DynamicProviderOptions } from './dynamic-provider-options'; 3 | 4 | export type ResolvedTypeProviders = { 5 | types: Type[]; 6 | } & Omit; 7 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: true, 6 | sourcemap: true, 7 | clean: true, 8 | format: ['esm', 'cjs'], 9 | platform: 'node', 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /src/definition/guard/is-dynamic-provider-options.ts: -------------------------------------------------------------------------------- 1 | import { DynamicProviderOptions } from '../dynamic-provider-options'; 2 | 3 | export function isDynamicProviderOptions( 4 | param: any[], 5 | ): param is DynamicProviderOptions[] { 6 | return typeof param[0] === 'object'; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Decorator 2 | export * from './decorator/inject-dynamic-providers'; 3 | 4 | // Interfaces and types 5 | export * from './definition/pattern'; 6 | export * from './definition/dynamic-provider-options'; 7 | 8 | // Utils 9 | export * from './utils/function/is-object'; 10 | -------------------------------------------------------------------------------- /test/__fixture__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './group-one/hippo.wild'; 2 | export * from './group-one/lion.wild'; 3 | export * from './group-two/cat.pet'; 4 | export * from './group-two/dog.pet'; 5 | export * from './not-for-search/veterinarian'; 6 | export * from './filter/play.command'; 7 | export * from './filter/command.constant'; 8 | -------------------------------------------------------------------------------- /test/__fixture__/filter/play.command.ts: -------------------------------------------------------------------------------- 1 | import { COMMAND_METADATA } from './command.constant'; 2 | 3 | export const COMMAND_NAME = 'NAME'; 4 | 5 | function Command(): ClassDecorator { 6 | return (target: TFunction): TFunction => { 7 | Reflect.defineMetadata(COMMAND_METADATA, {}, target); 8 | 9 | return target; 10 | }; 11 | } 12 | 13 | @Command() 14 | export class PlayCommand {} 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2017", 10 | "sourceMap": false, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": false, 14 | "skipLibCheck": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /src/definition/dynamic-provider-options.ts: -------------------------------------------------------------------------------- 1 | import { Pattern } from './pattern'; 2 | import { Scope, Type } from '@nestjs/common'; 3 | 4 | /** 5 | * Dynamic provide options. 6 | */ 7 | export interface DynamicProviderOptions { 8 | /** 9 | * Glob pattern for finding files. 10 | */ 11 | pattern: Pattern; 12 | 13 | /** 14 | * Specifies the lifetime of an injected Provider 15 | */ 16 | scope?: Scope; 17 | 18 | /** 19 | * Add providers to export in module. 20 | * 21 | * @default: false 22 | */ 23 | exportProviders?: boolean; 24 | 25 | /** 26 | * Custom filter for module file 27 | */ 28 | filterPredicate?: (types: Type) => boolean; 29 | } 30 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const eslint = require('@eslint/js'); 2 | const tseslint = require('typescript-eslint'); 3 | const eslintConfigPrettier = require('eslint-config-prettier'); 4 | 5 | module.exports = tseslint.config( 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | { 9 | languageOptions: { 10 | parserOptions: { 11 | project: 'tsconfig.json', 12 | sourceType: 'module', 13 | }, 14 | }, 15 | extends: [eslintConfigPrettier], 16 | rules: { 17 | '@typescript-eslint/interface-name-prefix': 'off', 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | '@typescript-eslint/explicit-module-boundary-types': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | }, 22 | }, 23 | { ignores: ['dist', 'node_modules', 'src/accounting-system-api/generated'] }, 24 | ); 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | 27 | # Install dependencies 28 | - run: npm install 29 | 30 | # Run tests 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /src/service/resolve-file.service.ts: -------------------------------------------------------------------------------- 1 | import { DynamicProviderOptions } from '../definition/dynamic-provider-options'; 2 | import { ResolvedTypeProviders } from '../definition/resolved-type-providers'; 3 | import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants'; 4 | import { Type } from '@nestjs/common'; 5 | import { glob } from 'glob'; 6 | import { IsObject } from '../utils/function/is-object'; 7 | import * as path from 'path'; 8 | 9 | export class ResolveFileService { 10 | async resolveByGlobPattern( 11 | startupPath: string, 12 | options: DynamicProviderOptions[], 13 | ): Promise { 14 | const classesList = await Promise.all( 15 | options.map(({ pattern, exportProviders, filterPredicate, scope }) => 16 | glob(pattern, { cwd: startupPath }).then(async (pathToFiles) => { 17 | const types = ( 18 | await this.getClasses(startupPath, pathToFiles) 19 | ).flat(); 20 | 21 | if (!filterPredicate) 22 | filterPredicate = (type) => 23 | IsObject(type) && 24 | Reflect.hasOwnMetadata(SCOPE_OPTIONS_METADATA, type); 25 | 26 | return { 27 | types: types.filter(filterPredicate), 28 | exportProviders, 29 | scope, 30 | }; 31 | }), 32 | ), 33 | ); 34 | 35 | return classesList.flat(); 36 | } 37 | 38 | private getClasses( 39 | startupPath: string, 40 | pathToFiles: string[], 41 | ): Promise { 42 | return Promise.all( 43 | pathToFiles.map(async (pathToFile: string) => { 44 | const resolvedPath = path.resolve(startupPath, pathToFile); 45 | const resolvedClass = await import(resolvedPath); 46 | 47 | return Object.values(resolvedClass); 48 | }), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-dynamic-providers", 3 | "version": "0.3.7", 4 | "description": "Inject dynamic providers", 5 | "author": "fjodor-rybakov", 6 | "license": "MIT", 7 | "main": "./dist/index.js", 8 | "module": "./dist/index.mjs", 9 | "types": "./dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.mjs", 14 | "require": "./dist/index.js" 15 | } 16 | }, 17 | "files": [ 18 | "dist/**/*", 19 | "*.md" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/fjodor-rybakov/nestjs-dynamic-providers" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "keywords": [ 29 | "nestjs", 30 | "decorator", 31 | "nest", 32 | "typescript", 33 | "glob", 34 | "pattern", 35 | "dynamic", 36 | "providers", 37 | "inject", 38 | "module" 39 | ], 40 | "scripts": { 41 | "build": "tsup", 42 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 43 | "lint": "eslint \"src/**/*.ts\" --fix", 44 | "test": "jest", 45 | "test:watch": "jest --watch", 46 | "test:cov": "jest --coverage", 47 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 48 | }, 49 | "dependencies": { 50 | "glob": "11.0.1" 51 | }, 52 | "peerDependencies": { 53 | "@nestjs/common": "9.* || 10.* || 11.*", 54 | "@nestjs/core": "9.* || 10.* || 11.*", 55 | "reflect-metadata": "^0.1.12 || ^0.2.0", 56 | "rxjs": "7.*" 57 | }, 58 | "devDependencies": { 59 | "@eslint/js": "^9.18.0", 60 | "@nestjs/common": "^11.0.4", 61 | "@nestjs/core": "^11.0.4", 62 | "@swc/core": "^1.10.9", 63 | "@types/jest": "^29.5.14", 64 | "@types/node": "^22.10.7", 65 | "@typescript-eslint/eslint-plugin": "^8.21.0", 66 | "@typescript-eslint/parser": "^8.21.0", 67 | "eslint": "^9.18.0", 68 | "eslint-config-prettier": "^10.0.1", 69 | "eslint-plugin-prettier": "^5.2.3", 70 | "jest": "^29.7.0", 71 | "prettier": "^3.4.2", 72 | "reflect-metadata": "^0.2.2", 73 | "rxjs": "^7.8.1", 74 | "ts-jest": "^29.2.5", 75 | "ts-loader": "^9.5.2", 76 | "tsconfig-paths": "^4.2.0", 77 | "tsup": "^8.3.5", 78 | "typescript": "^5.7.3", 79 | "typescript-eslint": "^8.21.0" 80 | }, 81 | "jest": { 82 | "moduleFileExtensions": [ 83 | "js", 84 | "json", 85 | "ts" 86 | ], 87 | "rootDir": "test", 88 | "testRegex": ".*\\.spec\\.ts$", 89 | "transform": { 90 | "^.+\\.(t|j)s$": "ts-jest" 91 | }, 92 | "collectCoverageFrom": [ 93 | "**/*.(t|j)s" 94 | ], 95 | "coverageDirectory": "../coverage", 96 | "testEnvironment": "node" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/decorator/inject-dynamic-providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Scope, Type, ClassProvider } from '@nestjs/common'; 2 | import { Pattern } from '../definition/pattern'; 3 | import { StoreValue } from '../definition/store-value'; 4 | import { DynamicProviderOptions } from '../definition/dynamic-provider-options'; 5 | import { ResolveFileService } from '../service/resolve-file.service'; 6 | import { isDynamicProviderOptions } from '../definition/guard/is-dynamic-provider-options'; 7 | import { isPattern } from '../definition/guard/is-pattern'; 8 | import * as process from 'process'; 9 | 10 | const store: StoreValue[] = []; 11 | 12 | /** 13 | * Add providers into module by glob pattern. 14 | */ 15 | export function InjectDynamicProviders( 16 | ...options: DynamicProviderOptions[] 17 | ): ClassDecorator; 18 | export function InjectDynamicProviders(...patterns: Pattern[]): ClassDecorator; 19 | export function InjectDynamicProviders(...options: any[]): ClassDecorator { 20 | return (target: TFunction): TFunction => { 21 | if (isDynamicProviderOptions(options)) store.push({ target, options }); 22 | if (isPattern(options)) 23 | store.push({ target, options: options.map((pattern) => ({ pattern })) }); 24 | 25 | return target; 26 | }; 27 | } 28 | 29 | /** 30 | * Init function. 31 | */ 32 | export async function resolveDynamicProviders( 33 | startupPath = process.cwd(), 34 | ): Promise { 35 | const resolveFileService = new ResolveFileService(); 36 | 37 | await Promise.all( 38 | store.map(async ({ target, options }) => { 39 | const resolvedProviders = await resolveFileService.resolveByGlobPattern( 40 | startupPath, 41 | options, 42 | ); 43 | 44 | resolvedProviders.forEach(({ types, exportProviders, scope }) => { 45 | mergeProviders(target, types, scope); 46 | if (exportProviders) { 47 | mergeExportedProviders(target, types); 48 | } 49 | }); 50 | }), 51 | ); 52 | } 53 | 54 | function mergeProviders( 55 | target: Function, 56 | newProviderTypes: Type[], 57 | scope?: Scope, 58 | ): void { 59 | const currentProviders = Reflect.getMetadata('providers', target) ?? []; 60 | const newProviders: (Provider | ClassProvider)[] = scope 61 | ? newProviderTypes.map((newClassType) => ({ 62 | provide: newClassType, 63 | useClass: newClassType, 64 | scope, 65 | })) 66 | : newProviderTypes; 67 | 68 | Reflect.defineMetadata( 69 | 'providers', 70 | [...currentProviders, ...newProviders], 71 | target, 72 | ); 73 | } 74 | 75 | function mergeExportedProviders( 76 | target: Function, 77 | newExportedProviders: Type[], 78 | ): void { 79 | const currentExportedProviders = Reflect.getMetadata('exports', target) ?? []; 80 | Reflect.defineMetadata( 81 | 'exports', 82 | [...currentExportedProviders, ...newExportedProviders], 83 | target, 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /test/decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectDynamicProviders, 3 | IsObject, 4 | resolveDynamicProviders, 5 | } from '../src'; 6 | import { 7 | Module, 8 | ModuleMetadata, 9 | Scope, 10 | Type, 11 | ClassProvider, 12 | } from '@nestjs/common'; 13 | import { 14 | Cat, 15 | COMMAND_METADATA, 16 | Dog, 17 | Hippo, 18 | Lion, 19 | PlayCommand, 20 | Veterinarian, 21 | } from './__fixture__'; 22 | import * as path from 'path'; 23 | import * as process from 'process'; 24 | 25 | describe('Dynamic module', () => { 26 | it('should set only injectable providers into module', async () => { 27 | @InjectDynamicProviders('test/__fixture__/**/*.wild.ts') 28 | @Module({}) 29 | class AnimalModule {} 30 | 31 | await resolveDynamicProviders(); 32 | 33 | const actualResult = Reflect.getMetadata('providers', AnimalModule); 34 | const expectResult = [Lion, Hippo]; 35 | 36 | expect(actualResult).toStrictEqual(expectResult); 37 | }); 38 | 39 | it('should concat providers in module', async () => { 40 | @InjectDynamicProviders('test/__fixture__/**/*.wild.ts') 41 | @Module({ providers: [Veterinarian] }) 42 | class AnimalModule {} 43 | 44 | await resolveDynamicProviders(); 45 | 46 | const actualResult = Reflect.getMetadata('providers', AnimalModule); 47 | const expectResult = [Veterinarian, Lion, Hippo]; 48 | 49 | expect(actualResult).toStrictEqual(expectResult); 50 | }); 51 | 52 | it('should find by two patterns', async () => { 53 | @InjectDynamicProviders( 54 | 'test/__fixture__/**/*.wild.ts', 55 | 'test/__fixture__/**/*.pet.ts', 56 | ) 57 | @Module({ providers: [Veterinarian] }) 58 | class AnimalModule {} 59 | 60 | await resolveDynamicProviders(); 61 | 62 | const actualResult = Reflect.getMetadata('providers', AnimalModule); 63 | const expectResult = [Veterinarian, Lion, Hippo, Dog, Cat]; 64 | 65 | expect(actualResult).toStrictEqual(expectResult); 66 | }); 67 | 68 | it('should add providers and set exported providers to exports into module', async () => { 69 | @InjectDynamicProviders( 70 | { 71 | pattern: 'test/__fixture__/**/*.wild.ts', 72 | exportProviders: true, 73 | }, 74 | { pattern: 'test/__fixture__/**/*.pet.ts' }, 75 | ) 76 | @Module({ providers: [Veterinarian] }) 77 | class AnimalModule {} 78 | 79 | await resolveDynamicProviders(); 80 | 81 | const actualResult: ModuleMetadata = { 82 | providers: Reflect.getMetadata('providers', AnimalModule), 83 | exports: Reflect.getMetadata('exports', AnimalModule), 84 | }; 85 | const expectResult: ModuleMetadata = { 86 | providers: [Veterinarian, Lion, Hippo, Dog, Cat], 87 | exports: [Lion, Hippo], 88 | }; 89 | 90 | expect(actualResult).toStrictEqual(expectResult); 91 | }); 92 | 93 | it('should replace default filter predicate', async () => { 94 | @InjectDynamicProviders({ 95 | pattern: 'test/__fixture__/**/*.command.ts', 96 | filterPredicate: (type: Type) => 97 | IsObject(type) && Reflect.hasOwnMetadata(COMMAND_METADATA, type), 98 | }) 99 | @Module({}) 100 | class CommandModule {} 101 | 102 | await resolveDynamicProviders(); 103 | 104 | const actualResult = Reflect.getMetadata('providers', CommandModule); 105 | const expectResult = [PlayCommand]; 106 | 107 | expect(actualResult).toStrictEqual(expectResult); 108 | }); 109 | 110 | it('should register providers with scope', async () => { 111 | @InjectDynamicProviders({ 112 | pattern: 'test/__fixture__/**/*.wild.ts', 113 | scope: Scope.TRANSIENT, 114 | }) 115 | @Module({}) 116 | class AnimalModule {} 117 | 118 | await resolveDynamicProviders(); 119 | 120 | const actualResult = Reflect.getMetadata('providers', AnimalModule); 121 | const expectResult: ClassProvider[] = [ 122 | { 123 | provide: Lion, 124 | useClass: Lion, 125 | scope: Scope.TRANSIENT, 126 | }, 127 | { 128 | provide: Hippo, 129 | useClass: Hippo, 130 | scope: Scope.TRANSIENT, 131 | }, 132 | ]; 133 | 134 | expect(actualResult).toStrictEqual(expectResult); 135 | }); 136 | 137 | it('should search providers from test folder', async () => { 138 | @InjectDynamicProviders('__fixture__/**/*.wild.ts') 139 | @Module({}) 140 | class AnimalModule {} 141 | 142 | await resolveDynamicProviders(path.resolve(process.cwd(), 'test')); 143 | 144 | const actualResult = Reflect.getMetadata('providers', AnimalModule); 145 | const expectResult = [Lion, Hippo]; 146 | 147 | expect(actualResult).toStrictEqual(expectResult); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | Package License 13 | 14 |

15 | 16 | ## 🧾 Description 17 | 18 | Do you have many classes of the same type and adding each of them to the module is burdensome? 19 | This package was created in order to be able to add providers to the module using the glob pattern. 20 | 21 | ## 👨🏻‍💻 Installation 22 | 23 | ```bash 24 | $ npm install nestjs-dynamic-providers 25 | ``` 26 | 27 | Or via yarn 28 | 29 | ```bash 30 | $ yarn add nestjs-dynamic-providers 31 | ``` 32 | 33 | ## ▶️ Usage 34 | 35 | > You may notice that files with `.ts` extension have a glob pattern is set for `.js`. This example assumes that you are 36 | > compiling files from `typescript` to `javascript`. This note does not apply for `ts-node`. 37 | 38 | > ⚠️**Important! Files are searched from the startup root(`process.cwd()`) by default. 39 | > To override this, you can pass startup path in `resolveDynamicProviders('my/startup/folder')` as argument.** 40 | 41 | First you need to call the initialization function in bootstrap. 42 | 43 | ```typescript 44 | /* main.ts */ 45 | 46 | import { AppModule } from './app.module'; 47 | import { NestFactory } from '@nestjs/core'; 48 | import { resolveDynamicProviders } from 'nestjs-dynamic-providers'; 49 | 50 | async function bootstrap() { 51 | await resolveDynamicProviders(); 52 | const app = await NestFactory.createApplicationContext(AppModule); 53 | await app.init(); 54 | } 55 | 56 | bootstrap(); 57 | ``` 58 | 59 | Then just add `@InjectDynamicProviders` decorator to the module. The sample below will add to the providers of the module 60 | all classes that it finds in files that end in `.animal.js`. 61 | 62 | > By default, classes are searched for that are marked with `@Injectable()` decorator. 63 | > To override you need to pass `filterPredicate` as parameters to `@InjectDynamicProviders()`. 64 | 65 | - `@InjectDynamicProviders` decorator takes list of glob patterns or list of options as parameters. 66 | 67 | ```typescript 68 | /* animal.module.ts */ 69 | 70 | import { Module } from '@nestjs/common'; 71 | import { InjectDynamicProviders } from 'nestjs-dynamic-providers'; 72 | import { AnyOtherProvider } from './any-other-provider'; 73 | 74 | @InjectDynamicProviders('dist/**/*.animal.js') 75 | @Module({ 76 | providers: [AnyOtherProvider], // Will be [AnyOtherProvider, Hippo, Lion] 77 | }) 78 | export class AnimalModule {} 79 | ``` 80 | 81 | ```typescript 82 | /* hippo.animal.ts */ 83 | 84 | import { Injectable } from '@nestjs/common'; 85 | 86 | @Injectable() 87 | export class Hippo {} 88 | ``` 89 | 90 | ```typescript 91 | /* lion.animal.ts */ 92 | 93 | import { Injectable } from '@nestjs/common'; 94 | 95 | @Injectable() 96 | export class Lion {} 97 | ``` 98 | 99 | ```typescript 100 | /* app.module.ts */ 101 | 102 | import { Module } from '@nestjs/common'; 103 | import { AnimalModule } from './animal.module'; 104 | 105 | @Module({ 106 | imports: [AnimalModule], 107 | }) 108 | export class AppModule {} 109 | ``` 110 | 111 | You can also add providers to exports. 112 | 113 | ```typescript 114 | /* animal.module.ts */ 115 | 116 | import { Module } from '@nestjs/common'; 117 | import { InjectDynamicProviders } from 'nestjs-dynamic-providers'; 118 | import { AnyOtherProvider } from './any-other-provider'; 119 | 120 | @InjectDynamicProviders({ pattern: 'dist/**/*.animal.js', exportProviders: true }) 121 | @Module({ 122 | providers: [AnyOtherProvider], // Will be [AnyOtherProvider, Hippo, Lion] 123 | exports: [AnyOtherProvider], // Will be [AnyOtherProvider, Hippo, Lion] 124 | }) 125 | export class AnimalModule {} 126 | ``` 127 | 128 | To override the search criteria for `class` in file, use `filterPredicart`. 129 | 130 | ```typescript 131 | /* bot.module.ts */ 132 | 133 | import { Module } from '@nestjs/common'; 134 | import { InjectDynamicProviders, IsObject } from 'nestjs-dynamic-providers'; 135 | 136 | @InjectDynamicProviders({ 137 | pattern: '**/*.command.js', 138 | filterPredicate: (type) => // Filter only object type and class has decorator with metadata key `ANY_METADATA_KEY` 139 | IsObject(type) && Reflect.hasMetadata('ANY_METADATA_KEY', type.prototype), 140 | }) 141 | @Module({}) 142 | export class BotModule {} 143 | ``` 144 | --------------------------------------------------------------------------------