├── .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 |
--------------------------------------------------------------------------------