|
1 | | -import { glob } from 'glob'; |
| 1 | +/* eslint-disable @typescript-eslint/no-var-requires */ |
| 2 | +import { globSync } from 'glob'; |
2 | 3 | import { resolve } from 'path'; |
3 | | -import { AUTO_CONTROLLER_WATERMARK, AUTO_INJECTABLE_WATERMARK } from '../interfaces'; |
4 | | -import 'reflect-metadata'; |
| 4 | +import { |
| 5 | + AUTO_ALIAS_WATERMARK, |
| 6 | + AUTO_CONTROLLER_WATERMARK, |
| 7 | + AUTO_INJECTABLE_WATERMARK, |
| 8 | + COMPONENT_SCAN_WATERMARK, |
| 9 | +} from '../interfaces'; |
| 10 | +import { Logger, Provider } from '@nestjs/common'; |
| 11 | +import { Type } from '@nestjs/common/interfaces/type.interface'; |
| 12 | +import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface'; |
| 13 | +import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface'; |
| 14 | +import { Abstract } from '@nestjs/common/interfaces/abstract.interface'; |
| 15 | +import { locate } from 'func-loc'; |
5 | 16 |
|
6 | 17 | type ClassType = new (...args: any[]) => any; |
7 | 18 |
|
8 | 19 | interface AutoClasses { |
9 | | - providers: ClassType[]; |
10 | | - controllers: ClassType[]; |
| 20 | + providers: Provider[]; |
| 21 | + controllers: Type[]; |
| 22 | + exports: Array< |
| 23 | + DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function |
| 24 | + >; |
11 | 25 | } |
12 | 26 |
|
13 | 27 | export class Importer { |
14 | | - private static instance: Importer; |
| 28 | + private rootPath = ''; |
15 | 29 |
|
16 | | - static getInstance(): Importer { |
17 | | - if (!Importer.instance) { |
18 | | - Importer.instance = new Importer(); |
19 | | - } |
20 | | - return Importer.instance; |
21 | | - } |
22 | | - |
23 | | - static async load(patterns: string[]): Promise<AutoClasses> { |
24 | | - const importer = Importer.getInstance(); |
25 | | - const pathNames = await importer.matchGlob(patterns); |
26 | | - const foundClasses = await Promise.all(pathNames.map((pathName) => importer.scan(pathName))); |
| 30 | + static load(patterns: string[]): AutoClasses { |
| 31 | + const importer = new Importer(); |
| 32 | + const pathNames = importer.matchGlob(patterns); |
| 33 | + const foundClasses = pathNames.map((pathName) => importer.scan(pathName)); |
27 | 34 | return foundClasses.reduce( |
28 | 35 | (merged, found) => ({ |
29 | 36 | providers: [...merged.providers, ...found.providers], |
30 | 37 | controllers: [...merged.controllers, ...found.controllers], |
| 38 | + exports: [...merged.exports, ...found.providers], |
31 | 39 | }), |
32 | | - { providers: [], controllers: [] } as AutoClasses, |
| 40 | + { providers: [], controllers: [], exports: [] }, |
33 | 41 | ); |
34 | 42 | } |
35 | 43 |
|
36 | | - private async scan(pathName: string): Promise<AutoClasses> { |
37 | | - const exports: Record<string, unknown> = await import(pathName); |
38 | | - const autoClasses = Object.values(exports).filter((value) => typeof value === 'function') as ClassType[]; |
| 44 | + private scan(pathName: string): AutoClasses { |
| 45 | + const exports: Record<string, unknown> = require(pathName); |
| 46 | + return (Object.values(exports) as ClassType[]).reduce( |
| 47 | + (classes: AutoClasses, value: ClassType) => { |
| 48 | + if (typeof value === 'function') { |
| 49 | + Reflect.hasMetadata(COMPONENT_SCAN_WATERMARK, value) && this.catchOverlappedScanScope(value, pathName); |
39 | 50 |
|
40 | | - return autoClasses.reduce( |
41 | | - (result: AutoClasses, value: ClassType) => { |
42 | | - Reflect.hasMetadata(AUTO_INJECTABLE_WATERMARK, value) && result.providers.push(value); |
43 | | - Reflect.hasMetadata(AUTO_CONTROLLER_WATERMARK, value) && result.controllers.push(value); |
44 | | - return result; |
| 51 | + if (Reflect.hasMetadata(AUTO_ALIAS_WATERMARK, value)) { |
| 52 | + classes.providers.push({ |
| 53 | + provide: Reflect.getMetadata(AUTO_ALIAS_WATERMARK, value), |
| 54 | + useClass: value, |
| 55 | + }); |
| 56 | + } else if (Reflect.hasMetadata(AUTO_INJECTABLE_WATERMARK, value)) { |
| 57 | + classes.providers.push(value); |
| 58 | + } else if (Reflect.hasMetadata(AUTO_CONTROLLER_WATERMARK, value)) { |
| 59 | + classes.controllers.push(value); |
| 60 | + } |
| 61 | + } |
| 62 | + return classes; |
45 | 63 | }, |
46 | | - { providers: [], controllers: [] }, |
| 64 | + { providers: [], controllers: [], exports: [] }, |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + private matchGlob(patterns: string[]) { |
| 69 | + const globs = patterns.map((pattern) => |
| 70 | + globSync(resolve(require.main?.path || process.cwd(), pattern), { |
| 71 | + ignore: ['**/node_modules/**'], |
| 72 | + }), |
47 | 73 | ); |
| 74 | + return globs.flat(); |
48 | 75 | } |
49 | 76 |
|
50 | | - private async matchGlob(patterns: string[]) { |
51 | | - const globs = patterns.map((pattern) => glob(resolve(process.cwd(), pattern))); |
52 | | - return (await Promise.all(globs)).flat(); |
| 77 | + /** |
| 78 | + * This code is intentionally structured to execute the callback function registered with `.then()` |
| 79 | + * and proceed through the event loop only after the asynchronous task of the `locate` function is completed. |
| 80 | + * Designed to handle potential errors after the scan is completed. |
| 81 | + */ |
| 82 | + private catchOverlappedScanScope(value: { new (...args: any[]): any; name: string }, pathName: string) { |
| 83 | + locate(value as any).then(({ path }: { path: string }) => { |
| 84 | + if (!this.rootPath) this.rootPath = pathName; |
| 85 | + if (this.rootPath !== path) { |
| 86 | + new Logger('ExceptionHandler').error( |
| 87 | + `ComponentScan() module scope cannot be overlapped.\n\nPotential causes:\n- A overlapped dependecy between modules.\n- Please check the module in '${this.rootPath}' and '${path}'\n\nScope [${value.name}]`, |
| 88 | + ); |
| 89 | + process.exit(1); |
| 90 | + } |
| 91 | + }); |
53 | 92 | } |
54 | 93 | } |
0 commit comments