├── .changeset ├── README.md └── config.json ├── .czrc ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ ├── cron.yml │ └── release.yml ├── .gitignore ├── .lintstagedrc ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src ├── debug.module.ts ├── exploration.module.ts ├── graphing.module.ts ├── index.ts ├── spelunker.interface.ts ├── spelunker.messages.ts └── spelunker.module.ts ├── test ├── bad-circular-dep │ ├── app.module.ts │ ├── bad-circular.e2e-spec.ts │ ├── bar.module.ts │ ├── bar.service.ts │ ├── foo.module.ts │ └── foo.service.ts ├── fixtures │ └── output.ts ├── good-circular-dep │ ├── app.module.ts │ ├── bar.module.ts │ ├── bar.service.ts │ ├── foo.module.ts │ ├── foo.service.ts │ └── good-circular.e2e-spec.ts ├── index.spec.ts └── large-app │ ├── animals │ ├── animals.controller.ts │ ├── animals.module.ts │ ├── animals.service.ts │ ├── cats │ │ ├── cats.controller.ts │ │ ├── cats.module.ts │ │ └── cats.service.ts │ ├── dogs │ │ ├── dogs.controller.ts │ │ ├── dogs.module.ts │ │ └── dogs.service.ts │ └── hamsters │ │ ├── hamsters.controller.ts │ │ ├── hamsters.module.ts │ │ └── hamsters.service.ts │ ├── app.e2e-spec.ts │ └── app.module.ts ├── tsconfig.build.json └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.7.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./node_modules/cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | plugins: ['@typescript-eslint', 'simple-import-sort'], 9 | parserOptions: { 10 | source: 'module', 11 | ecmaVersion: 2018, 12 | }, 13 | root: true, 14 | env: { 15 | node: true, 16 | }, 17 | rules: { 18 | 'no-control-regex': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-unused-vars': [ 24 | 'warn', 25 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 26 | ], 27 | 'simple-import-sort/imports': 'error', 28 | 'simple-import-sort/exports': 'error', 29 | // 'sort-imports': ['error', { ignoreDeclarationSort: true, ignoreCase: true }], 30 | 'prettier/prettier': 'warn', 31 | }, 32 | ignorePatterns: ['*.d.ts', 'dist/*', '**/node_modules/*', '*.js'], 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install pnpm 23 | run: npm i -g pnpm 24 | - name: install 25 | run: pnpm i --frozen-lockfile=false 26 | - name: Build 27 | run: pnpm build 28 | - name: test 29 | run: pnpm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: CRON Job 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x, 20.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Install pnpm 22 | run: npm i -g pnpm 23 | - name: install 24 | run: pnpm i --frozen-lockfile 25 | - name: Build 26 | run: pnpm build 27 | - name: test 28 | run: pnpm test 29 | env: 30 | CI: true 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 18.x 20 | uses: actions/setup-node@master 21 | with: 22 | node-version: 18.x 23 | 24 | - name: Install pnpm 25 | run: npm i -g pnpm 26 | 27 | - name: Install Dependencies 28 | run: pnpm i --frozen-lockfile=false 29 | 30 | - name: Build Projects 31 | run: pnpm build 32 | 33 | - name: Create Release Pull Request or Publish to npm 34 | id: changesets 35 | uses: changesets/action@v1 36 | with: 37 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 38 | publish: pnpm exec changeset publish 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "prettier --write", 4 | "eslint --fix" 5 | ], 6 | "*.{md,html,json,js}": [ 7 | "prettier --write" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="chore(release): %s :tada:" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.4.1](https://github.com/jmcdo29/nestjs-spelunker/compare/0.4.0...0.4.1) (2020-08-09) 2 | 3 | ## 1.3.2 4 | 5 | ### Patch Changes 6 | 7 | - b131996: Remove `reflect-metadata` from required peer dependencies list 8 | 9 | ## 1.3.1 10 | 11 | ### Patch Changes 12 | 13 | - e3ac557: do not display excluded modules on `imports` list of other modules 14 | 15 | ## 1.3.0 16 | 17 | ### Minor Changes 18 | 19 | - b6b6763: refactor: minor perf improvements and typing changes 20 | 21 | ## 1.2.0 22 | 23 | ### Minor Changes 24 | 25 | - ed0a420: support excluding modules from graph exploration via `ignoreImports` option 26 | 27 | ## 1.1.5 28 | 29 | ### Patch Changes 30 | 31 | - 5400375: Fix the fact that circular dependencies didn't actually log correctly with the debug module 32 | 33 | ## 1.1.4 34 | 35 | ### Patch Changes 36 | 37 | - 178c888: Allow value and factory providers to have falsy values instead of treating them as a class providers 38 | 39 | ## 1.1.3 40 | 41 | ### Patch Changes 42 | 43 | - b060d4b: docs: add sample source code & remove out-of-date notes 44 | 45 | ## 1.1.2 46 | 47 | ### Patch Changes 48 | 49 | - 347c65b: docs: minor improvements on code snippets & fix hyperlink 50 | 51 | ## 1.1.1 52 | 53 | ### Patch Changes 54 | 55 | - 58ae380: Removes cyclic dependencies caveat in README Graph Mode 56 | 57 | ## 1.1.0 58 | 59 | ### Minor Changes 60 | 61 | - 434eee4: Added graphing.module to generate a SpelunkedNode graph data structure and dependencies as an array of SpelunkedEdge objects. 62 | 63 | ## 1.0.0 64 | 65 | ### Major Changes 66 | 67 | - bdd36bc: First major version of the nestjs-spelunker package. 68 | Nest v8 is supported with it's new class token syntax 69 | pared down to just the class name. Circular dependencies 70 | that are not properly forwardReferenced will no longer 71 | crash the `debug` method. `@ogma/styler` is being used 72 | to color the output of the module in case of uncertain 73 | errors or improper tokens. 74 | 75 | ### Features 76 | 77 | - **module:** supports object exports ([66a21ef](https://github.com/jmcdo29/nestjs-spelunker/commit/66a21efc5bd335e0792b50c31e866f9407fdd80a)) 78 | 79 | # [0.4.0](https://github.com/jmcdo29/nestjs-spelunker/compare/0.3.0...0.4.0) (2020-08-09) 80 | 81 | ### Features 82 | 83 | - **module:** supports dynamic modules ([9e0664c](https://github.com/jmcdo29/nestjs-spelunker/commit/9e0664cb08a4a89e0d72933cbf56e35958ceda6b)) 84 | 85 | ### BREAKING CHANGES 86 | 87 | - **module:** The `debug` method is now asynchronous. 88 | This is due to the fact of needing to resolve promise 89 | based imports. 90 | 91 | # [0.3.0](https://github.com/jmcdo29/nestjs-spelunker/compare/0.2.1...0.3.0) (2020-08-08) 92 | 93 | ### Features 94 | 95 | - **module:** adds a debug method to print out modules and deps ([2d8510c](https://github.com/jmcdo29/nestjs-spelunker/commit/2d8510cffe07483521b531bc2760e79641423862)) 96 | 97 | ## [0.2.1](https://github.com/jmcdo29/nestjs-spelunker/compare/0.2.0...0.2.1) (2020-08-07) 98 | 99 | ### Features 100 | 101 | - change explore param type ([23acdd6](https://github.com/jmcdo29/nestjs-spelunker/commit/23acdd6144b9039c7b585f06db2c0efddf1a3f62)) 102 | 103 | # [0.2.0](https://github.com/jmcdo29/nestjs-spelunker/compare/0.1.0...0.2.0) (2020-06-25) 104 | 105 | ### Bug Fixes 106 | 107 | - **deps:** fixes build script ([bfe48ca](https://github.com/jmcdo29/nestjs-spelunker/commit/bfe48ca13e6b87e895d12972d0d7779ca0ba1fc2)) 108 | 109 | ### Features 110 | 111 | - **module:** changes the return to an object ([5b705c8](https://github.com/jmcdo29/nestjs-spelunker/commit/5b705c8b61f9daf3dba2f0e9da6fbf1218f0b4ce)) 112 | 113 | 114 | 115 | # 0.1.0 (2020-02-24) 116 | 117 | ### Features 118 | 119 | - **module:** implement the first iteration of the spelunker module ([5f50af2](https://github.com/jmcdo29/nestjs-spelunker/commit/5f50af2)) 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2019 Jay McDoniel, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS-Spelunker 2 | 3 | ## Description 4 | 5 | This module does a bit of a dive through the provided module and reads through the dependency tree from the point of entry given. It will find what a module `imports`, `provides`, has `controllers` for, and `exports` and will recursively search through the dependency tree until all modules have been scanned. For `providers` if there is a custom provider, the Spelunker will do its best to determine if Nest is to use a value, a class/standard, or a factory, and if a factory, what value is to be injected. 6 | 7 | ## Installation 8 | 9 | Pretty straightforward installation: 10 | 11 | ```sh 12 | npm i nestjs-spelunker 13 | yarn add nestjs-spelunker 14 | pnpm i nestjs-spelunker 15 | ``` 16 | 17 | ## Exploration Mode 18 | 19 | ### Exploration Usage 20 | 21 | Much like the [`SwaggerModule`](https://github.com/nestjs/swagger), the `SpelunkerModule` is not a module that you register within Nest's DI system, but rather use after the DI system has done all of the heavy lifting. Simple usage of the Spelunker could be like: 22 | 23 | ```ts 24 | // ... 25 | import { SpelunkerModule } from 'nestjs-spelunker'; 26 | 27 | async function bootstrap() { 28 | const app = await NestFactory.create(AppModule); 29 | // const app = await NestFactory.createApplicationContext(AppModule); 30 | console.log(SpelunkerModule.explore(app)); 31 | // ... 32 | } 33 | // ... 34 | ``` 35 | 36 | The `SpelunkerModule` will not get in the way of application bootstrapping, and will still allow for the server to listen. 37 | 38 | #### Excluding modules 39 | 40 | ```ts 41 | SpelunkerModule.explore(app, { 42 | // A list of regexes or predicate functions to apply over modules that will be ignored 43 | ignoreImports: [ 44 | /^TypeOrmModule/i, 45 | (moduleName) => moduleName.endsWith('something'), 46 | ], 47 | }) 48 | ``` 49 | 50 | ### Exploration Sample Output 51 | 52 | Given the following source code 53 | 54 |
55 | Sample code 56 | 57 | ```ts 58 | // main.ts 59 | import * as util from 'util' 60 | import { NestFactory } from '@nestjs/core' 61 | import { SpelunkerModule } from 'nestjs-spelunker' 62 | import { AppModule } from './app.module' 63 | 64 | async function bootstrap() { 65 | const app = await NestFactory.createApplicationContext(AppModule, { logger: false }) 66 | console.log( 67 | util.inspect( SpelunkerModule.explore(app), { depth: Infinity, colors: true } ) 68 | ) 69 | } 70 | bootstrap(); 71 | 72 | // src/app.module.ts 73 | import { Module, Injectable, Controller } from '@nestjs/common' 74 | 75 | @Controller('hamsters') 76 | export class HamstersController {} 77 | @Injectable() 78 | export class HamstersService {} 79 | 80 | @Module({ 81 | controllers: [HamstersController], 82 | providers: [HamstersService], 83 | }) 84 | export class HamstersModule {} 85 | 86 | 87 | @Controller('dogs') 88 | export class DogsController {} 89 | export class DogsService {} 90 | 91 | @Module({ 92 | controllers: [DogsController], 93 | providers: [ 94 | { 95 | provide: DogsService, 96 | inject: ['someString'], 97 | useFactory: (str: string) => new DogsService(), 98 | }, 99 | { 100 | provide: 'someString', 101 | useValue: 'my string', 102 | }, 103 | ], 104 | exports: [DogsService], 105 | }) 106 | export class DogsModule {} 107 | 108 | 109 | @Controller('cats') 110 | export class CatsController {} 111 | @Injectable() 112 | export class CatsService {} 113 | 114 | @Module({ 115 | controllers: [CatsController], 116 | providers: [CatsService], 117 | }) 118 | export class CatsModule {} 119 | 120 | 121 | export class AnimalsService {} 122 | @Controller('animals') 123 | export class AnimalsController {} 124 | 125 | @Module({ 126 | imports: [CatsModule, DogsModule, HamstersModule], 127 | controllers: [AnimalsController], 128 | providers: [ 129 | { 130 | provide: AnimalsService, 131 | useValue: new AnimalsService(), 132 | } 133 | ], 134 | exports: [DogsModule], 135 | }) 136 | export class AnimalsModule {} 137 | 138 | 139 | @Module({ 140 | imports: [AnimalsModule], 141 | }) 142 | export class AppModule {} 143 | ``` 144 | 145 |
146 | 147 | it outputs this: 148 | 149 | ```js 150 | [ 151 | { 152 | name: 'AppModule', 153 | imports: [ 'AnimalsModule' ], 154 | providers: {}, 155 | controllers: [], 156 | exports: [] 157 | }, 158 | { 159 | name: 'AnimalsModule', 160 | imports: [ 'CatsModule', 'DogsModule', 'HamstersModule' ], 161 | providers: { AnimalsService: { method: 'value' } }, 162 | controllers: [ 'AnimalsController' ], 163 | exports: [ 'DogsModule' ] 164 | }, 165 | { 166 | name: 'CatsModule', 167 | imports: [], 168 | providers: { CatsService: { method: 'standard' } }, 169 | controllers: [ 'CatsController' ], 170 | exports: [] 171 | }, 172 | { 173 | name: 'DogsModule', 174 | imports: [], 175 | providers: { 176 | DogsService: { method: 'factory', injections: [ 'someString' ] }, 177 | someString: { method: 'value' } 178 | }, 179 | controllers: [ 'DogsController' ], 180 | exports: [ 'DogsService' ] 181 | }, 182 | { 183 | name: 'HamstersModule', 184 | imports: [], 185 | providers: { HamstersService: { method: 'standard' } }, 186 | controllers: [ 'HamstersController' ], 187 | exports: [] 188 | } 189 | ] 190 | ``` 191 | 192 | In this example, `AppModule` imports `AnimalsModule`, and `AnimalsModule` imports `CatsModule`, `DogsModule`, and `HamstersModule` and each of those has its own set of `providers` and `controllers`. 193 | 194 | ## Graph Mode 195 | 196 | Sometimes you want to visualize the module inter-dependencies so you can better reason about them. The `SpelunkerModule` has a `graph` method that builds on the output of the `explore` method by generating a doubly-linked graph where each node represents a module and each edge a link to that module's dependencies or dependents. The `getEdges` method can traverse this graph from from the root (or any given) node, recursively following dependencies and returning a flat array of edges. These edges can be easily mapped to inputs for graphing tools, such as [Mermaid](https://mermaid-js.github.io/mermaid/#/). 197 | 198 | ### Graphing Usage 199 | 200 | Assume you have the sample output of the above `explore` section in a variable called tree. The following code will generate the list of edges suitable for pasting into a [Mermaid](https://mermaid-js.github.io/mermaid/#/) graph. 201 | 202 | ```ts 203 | const tree = SpelunkerModule.explore(app); 204 | const root = SpelunkerModule.graph(tree); 205 | const edges = SpelunkerModule.findGraphEdges(root); 206 | console.log('graph LR'); 207 | const mermaidEdges = edges.map( 208 | ({ from, to }) => ` ${from.module.name}-->${to.module.name}`, 209 | ); 210 | console.log(mermaidEdges.join('\n')); 211 | ``` 212 | 213 | ```mermaid 214 | graph LR 215 | AppModule-->AnimalsModule 216 | AnimalsModule-->CatsModule 217 | AnimalsModule-->DogsModule 218 | AnimalsModule-->HamstersModule 219 | ``` 220 | 221 | The edges can certainly be transformed into formats more suitable for other visualization tools. And the graph can be traversed with other strategies. 222 | 223 | ## Debug Mode 224 | 225 | Every now again again you may find yourself running into problems where Nest can't resolve a provider's dependencies. The `SpelunkerModule` has a `debug` method that's meant to help out with this kind of situation. 226 | 227 | ### Debug Usage 228 | 229 | Assume you have a `DogsModule` with the following information: 230 | 231 | ```ts 232 | @Module({ 233 | controller: [DogsController], 234 | exports: [DogsService], 235 | providers: [ 236 | { 237 | provide: 'someString', 238 | useValue: 'something', 239 | }, 240 | { 241 | provide: DogsService, 242 | inject: ['someString'], 243 | useFactory: (someStringInjection: string) => { 244 | return new DogsService(someStringInjection), 245 | }, 246 | } 247 | ] 248 | }) 249 | export class DogsModule {} 250 | ``` 251 | 252 | Now the `SpelunkerModule.debug()` method can be used anywhere with the `DogsModule` to get the dependency tree of the `DogsModule` including what the controller depends on, what imports are made, and what providers exist and their token dependencies. 253 | 254 | ```ts 255 | async function bootstrap() { 256 | const dogsDeps = await SpelunkerModule.debug(DogsModule); 257 | const app = await NestFactory.create(AppModule); 258 | await app.listen(3000); 259 | } 260 | ``` 261 | 262 | Because this method does not require the `INestApplicationContext` it can be used _before_ the `NestFactory` allowing you to have insight into what is being seen as the injection values and what's needed for the module to run. 263 | 264 | ### Debug Sample Output 265 | 266 | The output of the `debug()` method is an array of metadata, imports, controllers, exports, and providers. The `DogsModule` from above would look like this: 267 | 268 | ```js 269 | [ 270 | { 271 | name: 'DogsModule', 272 | imports: [], 273 | providers: [ 274 | { 275 | name: 'someString', 276 | dependencies: [], 277 | type: 'value', 278 | }, 279 | { 280 | name: 'DogsService', 281 | dependencies: ['someString'], 282 | type: 'factory', 283 | }, 284 | ], 285 | controllers: [ 286 | { 287 | name: 'DogsController', 288 | dependencies: ['DogsService'], 289 | }, 290 | ], 291 | exports: [ 292 | { 293 | name: 'DogsService', 294 | type: 'provider', 295 | }, 296 | ], 297 | }, 298 | ]; 299 | ``` 300 | 301 | ### Debug Messages 302 | 303 | If you are using the `debug` method and happen to have an invalid circular, the `SpelunkerModule` will write message to the log about the possibility of an unmarked circular dependency, meaning a missing `forwardRef` and the output will have `*****` in place of the `imports` where there's a problem reading the imported module. 304 | 305 | ## Caution 306 | 307 | This package is in early development, and any bugs found or improvements that can be thought of would be amazingly helpful. You can [log a bug here](../../issues/new), and you can reach out to me on Discord at [PerfectOrphan#6003](https://discordapp.com). 308 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'ci', 10 | 'chore', 11 | 'revert', 12 | 'feat', 13 | 'fix', 14 | 'improvement', 15 | 'docs', 16 | 'style', 17 | 'refactor', 18 | 'perf', 19 | 'test', 20 | ], 21 | ], 22 | 'scope-enum': [2, 'always', ['module', 'deps', 'docs', 'release']], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-spelunker", 3 | "version": "1.3.2", 4 | "description": "A NestJS Module to do a deep dive through the dependency tree structure and print the dependencies.", 5 | "author": { 6 | "name": "Jay McDoniel", 7 | "email": "me@jaymcdoneil.dev" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "main": "dist/index.js", 13 | "types": "dist/index.d.ts", 14 | "license": "MIT", 15 | "scripts": { 16 | "preversion": "yarn format && yarn lint && yarn build", 17 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 18 | "commit": "git-cz", 19 | "build": "nest build", 20 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 21 | "lint": "eslint '{src,test}/**/*.ts' --fix", 22 | "test": "c8 uvu -r ts-node/register test 'index\\.spec\\.ts$'" 23 | }, 24 | "devDependencies": { 25 | "@changesets/cli": "^2.21.1", 26 | "@commitlint/cli": "^16.2.3", 27 | "@commitlint/config-conventional": "^16.2.1", 28 | "@nestjs/cli": "^8.2.3", 29 | "@nestjs/common": "^8.4.1", 30 | "@nestjs/core": "^8.4.1", 31 | "@nestjs/platform-express": "^8.4.1", 32 | "@nestjs/schematics": "^8.0.8", 33 | "@nestjs/testing": "^8.4.1", 34 | "@ogma/nestjs-module": "^3.2.0", 35 | "@types/express": "^4.17.2", 36 | "@types/node": "^16", 37 | "@typescript-eslint/eslint-plugin": "^5.15.0", 38 | "@typescript-eslint/parser": "^5.15.0", 39 | "c8": "^7.11.0", 40 | "conventional-changelog-cli": "^2.0.31", 41 | "cz-conventional-changelog": "^3.1.0", 42 | "dequal": "^2.0.2", 43 | "eslint": "^8.11.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-plugin-import": "^2.19.1", 46 | "eslint-plugin-prettier": "^4.0.0", 47 | "eslint-plugin-simple-import-sort": "^7.0.0", 48 | "express": "^4.17.1", 49 | "husky": "^7.0.4", 50 | "lint-staged": "^12.3.7", 51 | "prettier": "^2.6.0", 52 | "reflect-metadata": "^0.1.13", 53 | "rxjs": "^7.5.5", 54 | "ts-node": "^10.9.1", 55 | "typescript": "^4.6.2", 56 | "uvu": "^0.5.3" 57 | }, 58 | "peerDependencies": { 59 | "@nestjs/common": ">6.11.0", 60 | "@nestjs/core": ">6.11.0" 61 | }, 62 | "dependencies": { 63 | "@ogma/styler": "^1.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/debug.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, ForwardReference, Type } from '@nestjs/common'; 2 | import { 3 | MODULE_METADATA, 4 | PARAMTYPES_METADATA, 5 | SELF_DECLARED_DEPS_METADATA, 6 | } from '@nestjs/common/constants'; 7 | 8 | import { 9 | CustomProvider, 10 | DebuggedExports, 11 | DebuggedProvider, 12 | DebuggedTree, 13 | ProviderType, 14 | } from './spelunker.interface'; 15 | import { UndefinedClassObject } from './spelunker.messages'; 16 | 17 | function isObject(val: any): val is Record { 18 | const isNil = val == null; 19 | return !isNil && typeof val === 'object'; 20 | } 21 | 22 | function hasProp( 23 | val: any, 24 | property: T, 25 | ): val is Record { 26 | return isObject(val) && Object.prototype.hasOwnProperty.call(val, property); 27 | } 28 | 29 | export class DebugModule { 30 | private static seenModules: Type[] = []; 31 | static async debug( 32 | modRef?: Type | DynamicModule | ForwardReference, 33 | importingModule?: string, 34 | ): Promise { 35 | const debuggedTree: DebuggedTree[] = []; 36 | if (modRef === undefined) { 37 | process.stdout.write( 38 | `The module "${importingModule}" is trying to import an undefined module. Do you have an unmarked circular dependency?\n`, 39 | ); 40 | return []; 41 | } 42 | if (typeof modRef === 'function') { 43 | debuggedTree.push(...(await this.getStandardModuleMetadata(modRef))); 44 | } else if (this.moduleIsForwardReference(modRef)) { 45 | const circMod = (modRef as any).forwardRef(); 46 | if (!this.seenModules.includes(circMod)) { 47 | this.seenModules.push(circMod); 48 | debuggedTree.push(...(await this.getStandardModuleMetadata(circMod))); 49 | } 50 | } else { 51 | debuggedTree.push(...(await this.getDynamicModuleMetadata(modRef))); 52 | } 53 | return debuggedTree.filter((item, index) => { 54 | const itemString = JSON.stringify(item); 55 | return ( 56 | index === 57 | debuggedTree.findIndex( 58 | (subItem) => itemString === JSON.stringify(subItem), 59 | ) 60 | ); 61 | }); 62 | } 63 | 64 | private static moduleIsForwardReference( 65 | modRef: DynamicModule | ForwardReference, 66 | ): modRef is ForwardReference { 67 | return Object.keys(modRef).includes('forwardRef'); 68 | } 69 | 70 | private static async getStandardModuleMetadata( 71 | modRef: Type, 72 | ): Promise { 73 | const imports: string[] = []; 74 | const providers: (DebuggedProvider & { type: ProviderType })[] = []; 75 | const controllers: DebuggedProvider[] = []; 76 | const exports: DebuggedExports[] = []; 77 | const subModules: DebuggedTree[] = []; 78 | for (const key of Reflect.getMetadataKeys(modRef)) { 79 | switch (key) { 80 | case MODULE_METADATA.IMPORTS: { 81 | const baseImports = this.getImports(modRef); 82 | for (const imp of baseImports) { 83 | subModules.push(...(await this.debug(imp, modRef.name))); 84 | } 85 | imports.push( 86 | ...(await Promise.all( 87 | baseImports.map(async (imp) => this.getImportName(imp)), 88 | )), 89 | ); 90 | break; 91 | } 92 | case MODULE_METADATA.PROVIDERS: { 93 | const baseProviders = 94 | Reflect.getMetadata(MODULE_METADATA.PROVIDERS, modRef) || []; 95 | providers.push(...this.getProviders(baseProviders)); 96 | break; 97 | } 98 | case MODULE_METADATA.CONTROLLERS: { 99 | const baseControllers = this.getController(modRef); 100 | const debuggedControllers = []; 101 | for (const controller of baseControllers) { 102 | debuggedControllers.push({ 103 | name: controller.name, 104 | dependencies: this.getDependencies(controller), 105 | }); 106 | } 107 | controllers.push(...debuggedControllers); 108 | break; 109 | } 110 | case MODULE_METADATA.EXPORTS: { 111 | const baseExports = this.getExports(modRef); 112 | exports.push( 113 | ...baseExports.map((exp) => ({ 114 | name: exp.name, 115 | type: this.exportType(exp), 116 | })), 117 | ); 118 | break; 119 | } 120 | } 121 | } 122 | return [ 123 | { 124 | name: modRef.name, 125 | imports, 126 | providers, 127 | controllers, 128 | exports, 129 | }, 130 | ].concat(subModules); 131 | } 132 | 133 | private static async getDynamicModuleMetadata( 134 | incomingModule: DynamicModule | Promise, 135 | ): Promise { 136 | const imports: string[] = []; 137 | const providers: (DebuggedProvider & { type: ProviderType })[] = []; 138 | const controllers: DebuggedProvider[] = []; 139 | const exports: DebuggedExports[] = []; 140 | const subModules: DebuggedTree[] = []; 141 | let modRef: DynamicModule; 142 | if ((incomingModule as Promise).then) { 143 | modRef = await incomingModule; 144 | } else { 145 | modRef = incomingModule as DynamicModule; 146 | } 147 | for (let imp of modRef.imports ?? []) { 148 | if (typeof imp === 'object') { 149 | imp = await this.resolveImport(imp); 150 | } 151 | subModules.push(...(await this.debug(imp as DynamicModule | Type))); 152 | imports.push(await this.getImportName(imp)); 153 | } 154 | providers.push( 155 | ...this.getProviders((modRef.providers as Type[]) || []), 156 | ); 157 | const debuggedControllers = []; 158 | for (const controller of modRef.controllers || []) { 159 | debuggedControllers.push({ 160 | name: controller.name, 161 | dependencies: this.getDependencies(controller), 162 | }); 163 | } 164 | controllers.push(...debuggedControllers); 165 | exports.push( 166 | ...(modRef.exports ?? []).map((exp) => ({ 167 | name: 168 | typeof exp === 'function' 169 | ? exp.name 170 | : // export is an object, not a string or class 171 | typeof exp === 'object' 172 | ? // object uses a class export 173 | ((exp as CustomProvider).provide as Type).name || 174 | // object uses a string/symbol export 175 | (exp as CustomProvider).provide.toString() 176 | : exp.toString(), 177 | type: this.exportType(exp as any), 178 | })), 179 | ); 180 | return [ 181 | { 182 | name: modRef.module.name, 183 | imports, 184 | providers, 185 | controllers, 186 | exports, 187 | }, 188 | ].concat(subModules); 189 | } 190 | 191 | private static async getImportName( 192 | imp: 193 | | Type 194 | | DynamicModule 195 | | Promise 196 | | ForwardReference, 197 | ): Promise { 198 | if (imp === undefined) { 199 | return '*********'; 200 | } 201 | let name = ''; 202 | const resolvedImp = await this.resolveImport(imp); 203 | if (typeof resolvedImp === 'function') { 204 | name = resolvedImp.name; 205 | } else { 206 | name = resolvedImp.module.name; 207 | } 208 | return name; 209 | } 210 | 211 | private static async resolveImport( 212 | imp: 213 | | Type 214 | | DynamicModule 215 | | Promise 216 | | ForwardReference, 217 | ): Promise> { 218 | return (imp as Promise).then 219 | ? await (imp as Promise) 220 | : (imp as ForwardReference).forwardRef 221 | ? (imp as ForwardReference).forwardRef() 222 | : (imp as Type); 223 | } 224 | 225 | private static getImports(modRef: Type): Array> { 226 | return Reflect.getMetadata(MODULE_METADATA.IMPORTS, modRef); 227 | } 228 | 229 | private static getController(modRef: Type): Array> { 230 | return Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, modRef); 231 | } 232 | 233 | private static getProviders( 234 | providers: Type[], 235 | ): (DebuggedProvider & { type: ProviderType })[] { 236 | const debuggedProviders: (DebuggedProvider & { 237 | type: ProviderType; 238 | })[] = []; 239 | for (const provider of providers) { 240 | let dependencies: () => any[]; 241 | // regular providers 242 | if (!this.isCustomProvider(provider)) { 243 | debuggedProviders.push({ 244 | name: provider.name, 245 | dependencies: this.getDependencies(provider), 246 | type: 'class', 247 | }); 248 | // custom providers 249 | } else { 250 | // set provide defaults 251 | const newProvider: DebuggedProvider & { 252 | type: ProviderType; 253 | } = { 254 | name: this.getProviderName(provider.provide), 255 | dependencies: [], 256 | type: 'class', 257 | }; 258 | if (hasProp(provider, 'useValue')) { 259 | newProvider.type = 'value'; 260 | dependencies = () => []; 261 | } else if (hasProp(provider, 'useFactory')) { 262 | newProvider.type = 'factory'; 263 | dependencies = () => 264 | (provider.inject ?? []).map(this.getProviderName); 265 | } else if (hasProp(provider, 'useClass')) { 266 | newProvider.type = 'class'; 267 | dependencies = () => this.getDependencies(provider.useClass); 268 | } else if (hasProp(provider, 'useExisting')) { 269 | newProvider.type = 'class'; 270 | dependencies = () => this.getDependencies(provider.useExisting); 271 | } else { 272 | throw new Error('Unknown provider type'); 273 | } 274 | newProvider.dependencies = dependencies(); 275 | debuggedProviders.push(newProvider); 276 | } 277 | } 278 | return debuggedProviders; 279 | } 280 | 281 | private static getExports(modRef: Type): Array> { 282 | return Reflect.getMetadata(MODULE_METADATA.EXPORTS, modRef); 283 | } 284 | 285 | private static getDependencies(classObj?: Type): Array { 286 | if (!classObj) { 287 | throw new Error(UndefinedClassObject); 288 | } 289 | const retDeps = []; 290 | const typedDeps = 291 | (Reflect.getMetadata(PARAMTYPES_METADATA, classObj) as Array< 292 | Type 293 | >) || []; 294 | for (const dep of typedDeps) { 295 | retDeps.push(dep?.name ?? 'UNKNOWN'); 296 | } 297 | const selfDeps = 298 | (Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, classObj) as [ 299 | // eslint-disable-next-line @typescript-eslint/ban-types 300 | { index: number; param: string | { forwardRef: () => Function } }, 301 | ]) || []; 302 | for (const selfDep of selfDeps) { 303 | let dep = selfDep.param; 304 | if (typeof dep === 'object') { 305 | dep = dep.forwardRef().name; 306 | } 307 | retDeps[selfDep.index] = dep; 308 | } 309 | if (retDeps.includes('UNKNOWN')) { 310 | process.stdout.write( 311 | `The provider "${classObj.name}" is trying to inject an undefined dependency. Do you have '@Inject(forwardRef())' in use here?\n`, 312 | ); 313 | } 314 | return retDeps; 315 | } 316 | 317 | private static getProviderName( 318 | provider: string | symbol | Type, 319 | ): string { 320 | return typeof provider === 'function' ? provider.name : provider.toString(); 321 | } 322 | 323 | private static isCustomProvider( 324 | provider: CustomProvider | Type, 325 | ): provider is CustomProvider { 326 | return (provider as any).provide; 327 | } 328 | 329 | private static exportType( 330 | classObj: Type | string | symbol, 331 | ): 'module' | 'provider' { 332 | let isModule = false; 333 | if (typeof classObj !== 'function') { 334 | return 'provider'; 335 | } 336 | for (const key of Object.keys(MODULE_METADATA)) { 337 | if ( 338 | Reflect.getMetadata( 339 | MODULE_METADATA[key as keyof typeof MODULE_METADATA], 340 | classObj, 341 | ) 342 | ) { 343 | isModule = true; 344 | } 345 | } 346 | return isModule ? 'module' : 'provider'; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/exploration.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INestApplicationContext, 3 | InjectionToken, 4 | OptionalFactoryDependency, 5 | } from '@nestjs/common'; 6 | import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; 7 | import { InternalCoreModule } from '@nestjs/core/injector/internal-core-module'; 8 | import { Module as NestModule } from '@nestjs/core/injector/module'; 9 | 10 | import { SpelunkedTree } from './spelunker.interface'; 11 | import { UndefinedProvider } from './spelunker.messages'; 12 | 13 | export type ExplorationOpts = { 14 | ignoreImports?: Array boolean)>; 15 | }; 16 | 17 | type ShouldIncludeModuleFn = (module: NestModule) => boolean; 18 | 19 | export class ExplorationModule { 20 | static explore( 21 | app: INestApplicationContext, 22 | opts?: ExplorationOpts, 23 | ): SpelunkedTree[] { 24 | const modulesArray = Array.from( 25 | ((app as any).container as NestContainer).getModules().values(), 26 | ); 27 | 28 | const ignoreImportsPredicateFns = (opts?.ignoreImports || []).map( 29 | (ignoreImportFnOrRegex) => 30 | ignoreImportFnOrRegex instanceof RegExp 31 | ? (moduleName: string) => ignoreImportFnOrRegex.test(moduleName) 32 | : ignoreImportFnOrRegex, 33 | ); 34 | const shouldIncludeModule: ShouldIncludeModuleFn = ( 35 | module: NestModule, 36 | ): boolean => { 37 | const moduleName = module.metatype.name; 38 | return ( 39 | module.metatype !== InternalCoreModule && 40 | !ignoreImportsPredicateFns.some((predicate) => predicate(moduleName)) 41 | ); 42 | }; 43 | 44 | // NOTE: Using for..of here instead of filter+map for performance reasons. 45 | const dependencyMap: SpelunkedTree[] = []; 46 | for (const nestjsModule of modulesArray) { 47 | if (shouldIncludeModule(nestjsModule)) { 48 | dependencyMap.push({ 49 | name: nestjsModule.metatype.name, 50 | imports: this.getImports(nestjsModule, shouldIncludeModule), 51 | providers: this.getProviders(nestjsModule), 52 | controllers: this.getControllers(nestjsModule), 53 | exports: this.getExports(nestjsModule), 54 | }); 55 | } 56 | } 57 | return dependencyMap; 58 | } 59 | 60 | private static getImports( 61 | module: NestModule, 62 | shouldIncludeModuleFn: ShouldIncludeModuleFn, 63 | ): string[] { 64 | // NOTE: Using for..of here instead of filter+map for performance reasons. 65 | const importsNames: string[] = []; 66 | for (const importedModule of module.imports.values()) { 67 | if (shouldIncludeModuleFn(importedModule)) { 68 | importsNames.push(importedModule.metatype.name); 69 | } 70 | } 71 | return importsNames; 72 | } 73 | 74 | private static getProviders(module: NestModule): SpelunkedTree['providers'] { 75 | const providerList: SpelunkedTree['providers'] = {}; 76 | // NOTE: Using for..of here instead of filter+forEach for performance reasons. 77 | for (const provider of module.providers.keys()) { 78 | if ( 79 | provider === module.metatype || 80 | provider === ModuleRef || 81 | provider === ApplicationConfig 82 | ) { 83 | continue; 84 | } 85 | 86 | const providerToken = this.getInjectionToken(provider); 87 | const providerInstanceWrapper = module.providers.get(provider); 88 | if (providerInstanceWrapper === undefined) { 89 | throw new Error(UndefinedProvider(providerToken)); 90 | } 91 | const metatype = providerInstanceWrapper.metatype; 92 | const name = (metatype && metatype.name) || 'useValue'; 93 | let provided: SpelunkedTree['providers'][number]; 94 | switch (name) { 95 | case 'useValue': 96 | provided = { 97 | method: 'value', 98 | }; 99 | break; 100 | case 'useClass': 101 | provided = { 102 | method: 'class', 103 | }; 104 | break; 105 | case 'useFactory': 106 | provided = { 107 | method: 'factory', 108 | injections: providerInstanceWrapper.inject?.map((injection) => 109 | this.getInjectionToken(injection), 110 | ), 111 | }; 112 | break; 113 | default: 114 | provided = { 115 | method: 'standard', 116 | }; 117 | } 118 | providerList[providerToken] = provided; 119 | } 120 | return providerList; 121 | } 122 | 123 | private static getControllers(module: NestModule): string[] { 124 | const controllersNames: string[] = []; 125 | for (const controller of module.controllers.values()) { 126 | controllersNames.push(controller.metatype.name); 127 | } 128 | return controllersNames; 129 | } 130 | 131 | private static getExports(module: NestModule): string[] { 132 | const exportsNames: string[] = []; 133 | for (const exportValue of module.exports.values()) { 134 | exportsNames.push(this.getInjectionToken(exportValue)); 135 | } 136 | return exportsNames; 137 | } 138 | 139 | private static getInjectionToken( 140 | injection: InjectionToken | OptionalFactoryDependency, 141 | ): string { 142 | return typeof injection === 'function' 143 | ? injection.name 144 | : this.tokenIsOptionalToken(injection) 145 | ? injection.token.toString() 146 | : injection.toString(); 147 | } 148 | 149 | private static tokenIsOptionalToken( 150 | token: InjectionToken | OptionalFactoryDependency, 151 | ): token is OptionalFactoryDependency { 152 | return !!(token as any)['token']; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/graphing.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SpelunkedEdge, 3 | SpelunkedNode, 4 | SpelunkedTree, 5 | } from './spelunker.interface'; 6 | 7 | export class GraphingModule { 8 | static graph(tree: SpelunkedTree[]): SpelunkedNode { 9 | const nodeMap = tree.reduce( 10 | (map, module) => 11 | map.set(module.name, { 12 | dependencies: new Set(), 13 | dependents: new Set(), 14 | module, 15 | }), 16 | new Map(), 17 | ); 18 | 19 | for (const [, node] of nodeMap) { 20 | this.findDependencies(node, nodeMap); 21 | } 22 | 23 | return this.findRoot(nodeMap); 24 | } 25 | 26 | static getEdges(root: SpelunkedNode): SpelunkedEdge[] { 27 | return [...this.getEdgesRecursively(root).values()]; 28 | } 29 | 30 | private static findDependencies( 31 | node: SpelunkedNode, 32 | nodeMap: Map, 33 | ): SpelunkedNode[] { 34 | return node.module.imports.map((m) => { 35 | const dependency = nodeMap.get(m); 36 | if (!dependency) throw new Error(`Unable to find ${m}!`); 37 | 38 | node.dependencies.add(dependency); 39 | dependency.dependents.add(node); 40 | 41 | return dependency; 42 | }); 43 | } 44 | 45 | /** 46 | * Find the root node, which is assumed to be the first node on which no other 47 | * nodes depend. If no such node exists, arbitrarily chose the first one as the 48 | * root. 49 | */ 50 | private static findRoot(nodeMap: Map): SpelunkedNode { 51 | const nodes = [...nodeMap.values()]; 52 | const root = nodes.find((n) => n.dependents.size === 0) ?? nodes[0]; 53 | 54 | if (!root) throw new Error('Unable to find root node'); 55 | 56 | return root; 57 | } 58 | 59 | private static getEdgesRecursively( 60 | root: SpelunkedNode, 61 | visitedNodes: Set = new Set(), 62 | ): Set { 63 | const set = new Set(); 64 | 65 | // short-circuit cycles 66 | if (visitedNodes.has(root)) return set; 67 | 68 | visitedNodes.add(root); 69 | 70 | for (const node of root.dependencies) { 71 | set.add({ from: root, to: node }); 72 | const edges = this.getEdgesRecursively(node, visitedNodes); 73 | for (const edge of edges) { 74 | set.add(edge); 75 | } 76 | } 77 | return set; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spelunker.interface'; 2 | export * from './spelunker.module'; 3 | -------------------------------------------------------------------------------- /src/spelunker.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | export interface SpelunkedTree { 4 | name: string; 5 | imports: string[]; 6 | exports: string[]; 7 | controllers: string[]; 8 | providers: Record; 9 | } 10 | 11 | interface SpelunkedProvider { 12 | method: 'value' | 'factory' | 'class' | 'standard'; 13 | injections?: string[]; 14 | } 15 | 16 | export type ProviderType = 'value' | 'factory' | 'class'; 17 | 18 | export interface DebuggedTree { 19 | name: string; 20 | imports: string[]; 21 | providers: Array; 22 | controllers: DebuggedProvider[]; 23 | exports: DebuggedExports[]; 24 | } 25 | 26 | export interface DebuggedProvider { 27 | name: string; 28 | dependencies: string[]; 29 | } 30 | 31 | export interface DebuggedExports { 32 | name: string; 33 | type: 'provider' | 'module'; 34 | } 35 | 36 | export interface CustomProvider { 37 | provide: Type | string | symbol; 38 | useClass?: Type; 39 | useValue?: any; 40 | useFactory?: (...args: any[]) => any; 41 | useExisting?: Type; 42 | inject?: any[]; 43 | } 44 | 45 | export interface SpelunkedNode { 46 | dependencies: Set; 47 | dependents: Set; 48 | module: SpelunkedTree; 49 | } 50 | 51 | export interface SpelunkedEdge { 52 | from: SpelunkedNode; 53 | to: SpelunkedNode; 54 | } 55 | -------------------------------------------------------------------------------- /src/spelunker.messages.ts: -------------------------------------------------------------------------------- 1 | import { style } from '@ogma/styler'; 2 | 3 | const gitRepoUrl = 'https://github.com/jmcdo29/nestjs-spelunker'; 4 | const minWtf = 'https://minimum-reproduction.wtf'; 5 | 6 | const baseMessage = style.whiteBg.black.apply( 7 | "I'm not sure how you did it, but you got to a point in the code where you shouldn't have been able to reach.", 8 | ); 9 | 10 | const newIssue = style.bBlue.apply( 11 | `Please open a Bug Report here: ${gitRepoUrl}/issues/new.`, 12 | ); 13 | const withMinRepro = style.cyan.apply( 14 | `If possible, please provide a minimum reproduction as well: ${minWtf}`, 15 | ); 16 | 17 | export const UndefinedClassObject = ` 18 | ${baseMessage} 19 | 20 | Somehow, the ${style.bold.apply('useClass')} and ${style.bold.apply( 21 | 'useExisting', 22 | )} options are both ${style.red.bold.apply( 23 | 'undefined', 24 | )} in a custom provider without a ${style.bold.apply( 25 | 'useFactory', 26 | )} or a ${style.bold.apply('useValue')}. 27 | 28 | 29 | ${newIssue} 30 | 31 | ${withMinRepro} 32 | `; 33 | 34 | export const UndefinedProvider = (provToken: string) => ` 35 | ${baseMessage} 36 | 37 | Somehow the token found for "${style.yellow.apply( 38 | provToken, 39 | )}" came back as ${style.red.bold.apply( 40 | 'undefined', 41 | )}. No idea how as this is all coming from Nest's internals in the first place. 42 | 43 | ${newIssue} 44 | 45 | ${withMinRepro} 46 | `; 47 | 48 | export const InvalidCircularModule = (moduleName: string) => 49 | `The module "${style.yellow.apply( 50 | moduleName, 51 | )}" is trying to import an undefined module. Do you have an unmarked circular dependency?`; 52 | -------------------------------------------------------------------------------- /src/spelunker.module.ts: -------------------------------------------------------------------------------- 1 | import { INestApplicationContext, Type } from '@nestjs/common'; 2 | 3 | import { DebugModule } from './debug.module'; 4 | import { ExplorationModule, ExplorationOpts } from './exploration.module'; 5 | import { GraphingModule } from './graphing.module'; 6 | import { 7 | DebuggedTree, 8 | SpelunkedEdge, 9 | SpelunkedNode, 10 | SpelunkedTree, 11 | } from './spelunker.interface'; 12 | 13 | export class SpelunkerModule { 14 | static explore( 15 | app: INestApplicationContext, 16 | opts?: ExplorationOpts, 17 | ): SpelunkedTree[] { 18 | return ExplorationModule.explore(app, opts); 19 | } 20 | 21 | static async debug(mod: Type): Promise { 22 | return DebugModule.debug(mod); 23 | } 24 | 25 | static graph(tree: SpelunkedTree[]): SpelunkedNode { 26 | return GraphingModule.graph(tree); 27 | } 28 | 29 | static findGraphEdges(root: SpelunkedNode): SpelunkedEdge[] { 30 | return GraphingModule.getEdges(root); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/bad-circular-dep/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { BarModule } from './bar.module'; 4 | import { FooModule } from './foo.module'; 5 | 6 | @Module({ 7 | imports: [FooModule, BarModule], 8 | }) 9 | export class AppModule {} 10 | -------------------------------------------------------------------------------- /test/bad-circular-dep/bad-circular.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import { ok } from 'uvu/assert'; 3 | 4 | import { SpelunkerModule } from '../../src'; 5 | import { AppModule } from './app.module'; 6 | 7 | export const BadCircularSuite = suite('BadCircularSuite'); 8 | 9 | BadCircularSuite('it should still print out a tree', async () => { 10 | const output = await SpelunkerModule.debug(AppModule); 11 | const containsUnknown = output.some((module) => 12 | module.providers.some((providers) => 13 | providers.dependencies.includes('UNKNOWN'), 14 | ), 15 | ); 16 | ok( 17 | containsUnknown, 18 | 'There should be an "UNKNOWN" in at least on of the modules[].providers[].dedpendencies', 19 | ); 20 | /*equal(output, [ 21 | { 22 | name: 'AppModule', 23 | imports: ['FooModule', 'BarModule'], 24 | providers: [], 25 | controllers: [], 26 | exports: [], 27 | }, 28 | { 29 | name: 'FooModule', 30 | imports: ['*********'], 31 | providers: [{ name: 'FooService', dependencies: [], type: 'class' }], 32 | controllers: [], 33 | exports: [{ name: 'FooService', type: 'provider' }], 34 | }, 35 | { 36 | name: 'BarModule', 37 | imports: ['FooModule'], 38 | providers: [{ name: 'BarService', dependencies: [], type: 'class' }], 39 | controllers: [], 40 | exports: [{ name: 'BarService', type: 'provider' }], 41 | }, 42 | ]);*/ 43 | }); 44 | -------------------------------------------------------------------------------- /test/bad-circular-dep/bar.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | 3 | import { BarService } from './bar.service'; 4 | import { FooModule } from './foo.module'; 5 | 6 | @Module({ 7 | imports: [forwardRef(() => FooModule)], 8 | providers: [BarService], 9 | exports: [BarService], 10 | }) 11 | export class BarModule {} 12 | -------------------------------------------------------------------------------- /test/bad-circular-dep/bar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { FooService } from './foo.service'; 4 | 5 | @Injectable() 6 | export class BarService { 7 | constructor(private readonly foo: FooService) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/bad-circular-dep/foo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { BarModule } from './bar.module'; 4 | import { FooService } from './foo.service'; 5 | 6 | @Module({ 7 | imports: [BarModule], 8 | providers: [FooService], 9 | exports: [FooService], 10 | }) 11 | export class FooModule {} 12 | -------------------------------------------------------------------------------- /test/bad-circular-dep/foo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { BarService } from './bar.service'; 4 | 5 | @Injectable() 6 | export class FooService { 7 | constructor(private readonly bar: BarService) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/output.ts: -------------------------------------------------------------------------------- 1 | import { DebuggedTree, SpelunkedTree } from '../../src'; 2 | 3 | export const exploreOutput: SpelunkedTree[] = [ 4 | { 5 | name: 'AppModule', 6 | imports: ['AnimalsModule', 'OgmaCoreModule'], 7 | providers: {}, 8 | controllers: [], 9 | exports: [], 10 | }, 11 | { 12 | name: 'AnimalsModule', 13 | imports: ['CatsModule', 'DogsModule', 'HamstersModule', 'OgmaCoreModule'], 14 | providers: { 15 | AnimalsService: { 16 | method: 'value', 17 | }, 18 | }, 19 | controllers: ['AnimalsController'], 20 | exports: ['DogsModule'], 21 | }, 22 | { 23 | name: 'CatsModule', 24 | imports: ['OgmaCoreModule'], 25 | providers: { 26 | CatsService: { 27 | method: 'standard', 28 | }, 29 | }, 30 | controllers: ['CatsController'], 31 | exports: [], 32 | }, 33 | { 34 | name: 'DogsModule', 35 | imports: ['OgmaCoreModule'], 36 | providers: { 37 | someString: { 38 | method: 'value', 39 | }, 40 | DogsService: { 41 | method: 'factory', 42 | injections: ['someString'], 43 | }, 44 | }, 45 | controllers: ['DogsController'], 46 | exports: ['DogsService'], 47 | }, 48 | { 49 | name: 'HamstersModule', 50 | imports: ['OgmaCoreModule'], 51 | providers: { 52 | HamstersService: { 53 | method: 'standard', 54 | }, 55 | }, 56 | controllers: ['HamstersController'], 57 | exports: [], 58 | }, 59 | ]; 60 | 61 | export const debugOutput: DebuggedTree[] = [ 62 | { 63 | name: 'AppModule', 64 | imports: ['AnimalsModule', 'OgmaCoreModule'], 65 | providers: [], 66 | controllers: [], 67 | exports: [], 68 | }, 69 | { 70 | name: 'AnimalsModule', 71 | imports: ['CatsModule', 'DogsModule', 'HamstersModule'], 72 | providers: [ 73 | { 74 | name: 'AnimalsService', 75 | dependencies: [], 76 | type: 'value', 77 | }, 78 | ], 79 | controllers: [ 80 | { 81 | name: 'AnimalsController', 82 | dependencies: ['AnimalsService'], 83 | }, 84 | ], 85 | exports: [ 86 | { 87 | name: 'DogsModule', 88 | type: 'module', 89 | }, 90 | ], 91 | }, 92 | { 93 | name: 'CatsModule', 94 | imports: [], 95 | providers: [ 96 | { 97 | name: 'CatsService', 98 | dependencies: [], 99 | type: 'class', 100 | }, 101 | ], 102 | controllers: [ 103 | { 104 | name: 'CatsController', 105 | dependencies: ['CatsService'], 106 | }, 107 | ], 108 | exports: [], 109 | }, 110 | { 111 | name: 'DogsModule', 112 | imports: [], 113 | providers: [ 114 | { 115 | name: 'someString', 116 | dependencies: [], 117 | type: 'value', 118 | }, 119 | { 120 | name: 'DogsService', 121 | dependencies: ['someString'], 122 | type: 'factory', 123 | }, 124 | ], 125 | controllers: [ 126 | { 127 | name: 'DogsController', 128 | dependencies: ['DogsService'], 129 | }, 130 | ], 131 | exports: [ 132 | { 133 | name: 'DogsService', 134 | type: 'provider', 135 | }, 136 | ], 137 | }, 138 | { 139 | name: 'HamstersModule', 140 | imports: [], 141 | providers: [ 142 | { 143 | name: 'HamstersService', 144 | dependencies: [], 145 | type: 'class', 146 | }, 147 | ], 148 | controllers: [ 149 | { 150 | name: 'HamstersController', 151 | dependencies: ['HamstersService'], 152 | }, 153 | ], 154 | exports: [], 155 | }, 156 | { 157 | name: 'OgmaCoreModule', 158 | imports: [], 159 | providers: [ 160 | { 161 | name: 'OGMA_OPTIONS', 162 | dependencies: [], 163 | type: 'value', 164 | }, 165 | { 166 | name: 'OGMA_INTERCEPTOR_OPTIONS', 167 | dependencies: ['OGMA_OPTIONS'], 168 | type: 'factory', 169 | }, 170 | { 171 | name: 'OGMA_SERVICE_OPTIONS', 172 | dependencies: ['OGMA_OPTIONS'], 173 | type: 'factory', 174 | }, 175 | { 176 | name: 'OGMA_TRACE_METHOD_OPTION', 177 | dependencies: ['OGMA_SERVICE_OPTIONS'], 178 | type: 'factory', 179 | }, 180 | { 181 | name: 'OGMA_INSTANCE', 182 | dependencies: ['OGMA_SERVICE_OPTIONS'], 183 | type: 'factory', 184 | }, 185 | { 186 | name: 'HttpInterceptorService', 187 | dependencies: ['OGMA_INTERCEPTOR_OPTIONS', 'Reflector'], 188 | type: 'factory', 189 | }, 190 | { 191 | name: 'WebsocketInterceptorService', 192 | dependencies: ['OGMA_INTERCEPTOR_OPTIONS', 'Reflector'], 193 | type: 'factory', 194 | }, 195 | { 196 | name: 'GqlInterceptorService', 197 | dependencies: ['OGMA_INTERCEPTOR_OPTIONS', 'Reflector'], 198 | type: 'factory', 199 | }, 200 | { 201 | name: 'RpcInterceptorService', 202 | dependencies: ['OGMA_INTERCEPTOR_OPTIONS', 'Reflector'], 203 | type: 'factory', 204 | }, 205 | { 206 | name: 'OgmaService', 207 | dependencies: [ 208 | 'OGMA_INSTANCE', 209 | 'OGMA_CONTEXT', 210 | 'Object', 211 | 'OGMA_TRACE_METHOD_OPTION', 212 | ], 213 | type: 'class', 214 | }, 215 | { 216 | name: 'DelegatorService', 217 | dependencies: [ 218 | 'HttpInterceptorService', 219 | 'WebsocketInterceptorService', 220 | 'RpcInterceptorService', 221 | 'GqlInterceptorService', 222 | ], 223 | type: 'class', 224 | }, 225 | ], 226 | controllers: [], 227 | exports: [ 228 | { 229 | name: 'OGMA_INSTANCE', 230 | type: 'provider', 231 | }, 232 | { 233 | name: 'OGMA_INTERCEPTOR_OPTIONS', 234 | type: 'provider', 235 | }, 236 | { 237 | name: 'OgmaService', 238 | type: 'provider', 239 | }, 240 | { 241 | name: 'DelegatorService', 242 | type: 'provider', 243 | }, 244 | { 245 | name: 'HttpInterceptorService', 246 | type: 'provider', 247 | }, 248 | { 249 | name: 'GqlInterceptorService', 250 | type: 'provider', 251 | }, 252 | { 253 | name: 'RpcInterceptorService', 254 | type: 'provider', 255 | }, 256 | { 257 | name: 'WebsocketInterceptorService', 258 | type: 'provider', 259 | }, 260 | ], 261 | }, 262 | ]; 263 | 264 | export const graphEdgesOutput = [ 265 | 'AppModule-->AnimalsModule', 266 | 'AnimalsModule-->CatsModule', 267 | 'CatsModule-->OgmaCoreModule', 268 | 'AnimalsModule-->DogsModule', 269 | 'DogsModule-->OgmaCoreModule', 270 | 'AnimalsModule-->HamstersModule', 271 | 'HamstersModule-->OgmaCoreModule', 272 | 'AnimalsModule-->OgmaCoreModule', 273 | 'AppModule-->OgmaCoreModule', 274 | ]; 275 | 276 | export const cyclicGraphEdgesOutput = [ 277 | 'AppModule-->AnimalsModule', 278 | 'AnimalsModule-->CatsModule', 279 | 'CatsModule-->OgmaCoreModule', 280 | 'OgmaCoreModule-->AppModule', 281 | 'AnimalsModule-->DogsModule', 282 | 'DogsModule-->OgmaCoreModule', 283 | 'AnimalsModule-->HamstersModule', 284 | 'HamstersModule-->OgmaCoreModule', 285 | 'AnimalsModule-->OgmaCoreModule', 286 | 'AppModule-->OgmaCoreModule', 287 | ]; 288 | -------------------------------------------------------------------------------- /test/good-circular-dep/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { BarModule } from './bar.module'; 4 | import { FooModule } from './foo.module'; 5 | 6 | @Module({ 7 | imports: [FooModule, BarModule], 8 | }) 9 | export class AppModule {} 10 | -------------------------------------------------------------------------------- /test/good-circular-dep/bar.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | 3 | import { BarService } from './bar.service'; 4 | import { FooModule } from './foo.module'; 5 | 6 | @Module({ 7 | imports: [forwardRef(() => FooModule)], 8 | providers: [BarService], 9 | exports: [BarService], 10 | }) 11 | export class BarModule {} 12 | -------------------------------------------------------------------------------- /test/good-circular-dep/bar.service.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Inject, Injectable } from '@nestjs/common'; 2 | 3 | import { FooService } from './foo.service'; 4 | 5 | @Injectable() 6 | export class BarService { 7 | constructor( 8 | @Inject(forwardRef(() => FooService)) private readonly foo: FooService, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /test/good-circular-dep/foo.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | 3 | import { BarModule } from './bar.module'; 4 | import { FooService } from './foo.service'; 5 | 6 | @Module({ 7 | imports: [forwardRef(() => BarModule)], 8 | providers: [FooService], 9 | exports: [FooService], 10 | }) 11 | export class FooModule {} 12 | -------------------------------------------------------------------------------- /test/good-circular-dep/foo.service.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Inject, Injectable } from '@nestjs/common'; 2 | 3 | import { BarService } from './bar.service'; 4 | 5 | @Injectable() 6 | export class FooService { 7 | constructor( 8 | @Inject(forwardRef(() => BarService)) private readonly bar: BarService, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /test/good-circular-dep/good-circular.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { ok } from 'assert'; 2 | import { suite } from 'uvu'; 3 | import { unreachable } from 'uvu/assert'; 4 | 5 | import { DebuggedTree, SpelunkerModule } from '../../src'; 6 | import { AppModule } from './app.module'; 7 | 8 | type ServiceHasDependencyOptions = { 9 | moduleName: string; 10 | serviceName: string; 11 | dependencyName: string; 12 | }; 13 | 14 | const serviceHasDependency = ( 15 | output: DebuggedTree[], 16 | options: ServiceHasDependencyOptions, 17 | ): void => { 18 | const module = output.find((mod) => mod.name === options.moduleName); 19 | if (!module) { 20 | unreachable(`Module ${options.moduleName} was not in the debugged tree`); 21 | } 22 | const service = module!.providers.find( 23 | (provider) => provider.name === options.serviceName, 24 | ); 25 | if (!service) { 26 | unreachable( 27 | `Provider ${options.serviceName} was not found in the module ${options.moduleName}`, 28 | ); 29 | } 30 | const serviceHasDep = service!.dependencies.find( 31 | (dep) => dep === options.dependencyName, 32 | ); 33 | ok( 34 | serviceHasDep, 35 | `Dependency ${options.dependencyName} was not found for provider ${options.serviceName}`, 36 | ); 37 | }; 38 | 39 | export const GoodCircularSuite = suite('GoodCircularSuite'); 40 | 41 | GoodCircularSuite('it should still print out a tree', async () => { 42 | const output = await SpelunkerModule.debug(AppModule); 43 | ok(output); 44 | serviceHasDependency(output, { 45 | moduleName: 'FooModule', 46 | serviceName: 'FooService', 47 | dependencyName: 'BarService', 48 | }); 49 | serviceHasDependency(output, { 50 | moduleName: 'BarModule', 51 | serviceName: 'BarService', 52 | dependencyName: 'FooService', 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadCircularSuite } from './bad-circular-dep/bad-circular.e2e-spec'; 2 | import { GoodCircularSuite } from './good-circular-dep/good-circular.e2e-spec'; 3 | import { SpelunkerSuite } from './large-app/app.e2e-spec'; 4 | 5 | SpelunkerSuite.run(); 6 | BadCircularSuite.run(); 7 | GoodCircularSuite.run(); 8 | -------------------------------------------------------------------------------- /test/large-app/animals/animals.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | import { AnimalsService } from './animals.service'; 4 | 5 | @Controller('animals') 6 | export class AnimalsController { 7 | constructor(private readonly animalsService: AnimalsService) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/large-app/animals/animals.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AnimalsController } from './animals.controller'; 4 | import { AnimalsService } from './animals.service'; 5 | import { CatsModule } from './cats/cats.module'; 6 | import { DogsModule } from './dogs/dogs.module'; 7 | import { HamstersModule } from './hamsters/hamsters.module'; 8 | 9 | @Module({ 10 | imports: [CatsModule, DogsModule, HamstersModule], 11 | controllers: [AnimalsController], 12 | providers: [ 13 | { 14 | provide: AnimalsService, 15 | useValue: new AnimalsService(), 16 | }, 17 | ], 18 | exports: [DogsModule], 19 | }) 20 | export class AnimalsModule {} 21 | -------------------------------------------------------------------------------- /test/large-app/animals/animals.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AnimalsService {} 5 | -------------------------------------------------------------------------------- /test/large-app/animals/cats/cats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Inject } from '@nestjs/common'; 2 | 3 | import { CatsService } from './cats.service'; 4 | 5 | @Controller('cats') 6 | export class CatsController { 7 | constructor( 8 | @Inject(CatsService.name) private readonly catService: CatsService, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /test/large-app/animals/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CatsController } from './cats.controller'; 4 | import { CatsService } from './cats.service'; 5 | 6 | @Module({ 7 | controllers: [CatsController], 8 | providers: [ 9 | { 10 | provide: CatsService.name, 11 | useClass: CatsService, 12 | }, 13 | ], 14 | }) 15 | export class CatsModule {} 16 | -------------------------------------------------------------------------------- /test/large-app/animals/cats/cats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class CatsService {} 5 | -------------------------------------------------------------------------------- /test/large-app/animals/dogs/dogs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | import { DogsService } from './dogs.service'; 4 | 5 | @Controller('dogs') 6 | export class DogsController { 7 | constructor(private readonly dogService: DogsService) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/large-app/animals/dogs/dogs.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DogsController } from './dogs.controller'; 4 | import { DogsService } from './dogs.service'; 5 | 6 | @Module({ 7 | controllers: [DogsController], 8 | providers: [ 9 | { 10 | provide: 'someString', 11 | useValue: 'something', 12 | }, 13 | { 14 | provide: DogsService, 15 | useFactory: (_something: string) => new DogsService(), 16 | inject: ['someString'], 17 | }, 18 | ], 19 | exports: [DogsService], 20 | }) 21 | export class DogsModule {} 22 | -------------------------------------------------------------------------------- /test/large-app/animals/dogs/dogs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class DogsService {} 5 | -------------------------------------------------------------------------------- /test/large-app/animals/hamsters/hamsters.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | import { HamstersService } from './hamsters.service'; 4 | 5 | @Controller('hamsters') 6 | export class HamstersController { 7 | constructor(private readonly hamsterService: HamstersService) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/large-app/animals/hamsters/hamsters.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { HamstersController } from './hamsters.controller'; 4 | import { HamstersService } from './hamsters.service'; 5 | 6 | @Module({ 7 | controllers: [HamstersController], 8 | providers: [HamstersService], 9 | }) 10 | export class HamstersModule {} 11 | -------------------------------------------------------------------------------- /test/large-app/animals/hamsters/hamsters.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class HamstersService {} 5 | -------------------------------------------------------------------------------- /test/large-app/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { dequal } from 'dequal'; 4 | import { suite } from 'uvu'; 5 | import { equal, is } from 'uvu/assert'; 6 | 7 | import { SpelunkerModule } from '../../src/'; 8 | import { 9 | cyclicGraphEdgesOutput, 10 | debugOutput, 11 | exploreOutput, 12 | graphEdgesOutput, 13 | } from '../fixtures/output'; 14 | import { AppModule } from './app.module'; 15 | 16 | export const SpelunkerSuite = 17 | suite<{ app: INestApplication }>('SpelunkerSuite'); 18 | 19 | SpelunkerSuite.before(async (context) => { 20 | context.app = await NestFactory.create(AppModule, { logger: false }); 21 | }); 22 | SpelunkerSuite.after(async ({ app }) => app.close()); 23 | 24 | SpelunkerSuite('Should allow the spelunkerModule to explore', ({ app }) => { 25 | const output = SpelunkerModule.explore(app); 26 | exploreOutput.forEach((expected) => { 27 | is( 28 | output.some((outputPart) => { 29 | return dequal(outputPart, expected); 30 | }), 31 | true, 32 | ); 33 | }); 34 | }); 35 | 36 | SpelunkerSuite('Should allow the SpelunkerModule to debug', async () => { 37 | const output = await SpelunkerModule.debug(AppModule); 38 | equal(output, debugOutput); 39 | }); 40 | 41 | SpelunkerSuite('Should allow the SpelunkerModule to graph', ({ app }) => { 42 | const tree = SpelunkerModule.explore(app); 43 | const root = SpelunkerModule.graph(tree); 44 | const edges = SpelunkerModule.findGraphEdges(root); 45 | equal( 46 | edges.map((e) => `${e.from.module.name}-->${e.to.module.name}`), 47 | graphEdgesOutput, 48 | ); 49 | }); 50 | 51 | SpelunkerSuite( 52 | 'Should handle a module circular dependency when finding graph edges', 53 | ({ app }) => { 54 | const tree = SpelunkerModule.explore(app); 55 | tree.slice(-1)[0].imports.push('AppModule'); 56 | const root = SpelunkerModule.graph(tree); 57 | const edges = SpelunkerModule.findGraphEdges(root); 58 | equal( 59 | edges.map((e) => `${e.from.module.name}-->${e.to.module.name}`), 60 | cyclicGraphEdgesOutput, 61 | ); 62 | }, 63 | ); 64 | 65 | SpelunkerSuite( 66 | 'Should exclude modules according to the `ignoreImports` option', 67 | ({ app }) => { 68 | const emptyTree = SpelunkerModule.explore(app, { 69 | ignoreImports: [ 70 | // for type-sake coverage only 71 | /^this_will_not_pass$/, 72 | // for type-sake coverage only 73 | (moduleName) => moduleName.startsWith('this_will_not_pass_either'), 74 | // excluding everything 75 | /.+/, 76 | ], 77 | }); 78 | equal(emptyTree.length, 0); 79 | 80 | const nonEmptyTree = SpelunkerModule.explore(app, { 81 | ignoreImports: [(moduleName) => moduleName.includes('Core')], 82 | }); 83 | equal(nonEmptyTree.map((module) => module.name).sort(), [ 84 | 'AnimalsModule', 85 | 'AppModule', 86 | 'CatsModule', 87 | 'DogsModule', 88 | 'HamstersModule', 89 | ]); 90 | }, 91 | ); 92 | -------------------------------------------------------------------------------- /test/large-app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OgmaModule } from '@ogma/nestjs-module'; 3 | 4 | import { AnimalsModule } from './animals/animals.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | AnimalsModule, 9 | OgmaModule.forRoot({ 10 | interceptor: false, 11 | }), 12 | ], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true, 13 | "strict": true 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------