Skip to content

Commit

Permalink
feat(module): adds a debug method to print out modules and deps
Browse files Browse the repository at this point in the history
`SpelunkerModule.debug()` is now an available method that can take in a module. This function will
output a JSON with a module's imports, providers, provider type, dependencies, controllers,
controller deps, and exports along with the export types.
  • Loading branch information
jmcdo29 committed Aug 8, 2020
1 parent 040b3d1 commit 2d8510c
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 92 deletions.
160 changes: 160 additions & 0 deletions lib/debug.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Type } from '@nestjs/common';
import { MODULE_METADATA } from '@nestjs/common/constants';
import {
DebuggedTree,
DebuggedProvider,
ProviderType,
CustomProvider,
DebuggedExports,
} from './spelunker.interface';

export class DebugModule {
static debug(app: Type<any>): DebuggedTree[] {
const debuggedTree: DebuggedTree[] = [];
const imports: string[] = [];
const providers: (DebuggedProvider & { type: ProviderType })[] = [];
const controllers: DebuggedProvider[] = [];
const exports: DebuggedExports[] = [];
for (const key of Reflect.getMetadataKeys(app)) {
switch (key) {
case MODULE_METADATA.IMPORTS:
const baseImports = DebugModule.getImports(app);
for (const imp of baseImports) {
debuggedTree.push(...DebugModule.debug(imp));
}
imports.push(...baseImports.map((imp) => imp.name));
break;
case MODULE_METADATA.PROVIDERS:
providers.push(...DebugModule.getProviders(app));
break;
case MODULE_METADATA.CONTROLLERS:
const baseControllers = DebugModule.getController(app);
const debuggedControllers = [];
for (const controller of baseControllers) {
debuggedControllers.push({
name: controller.name,
dependencies: DebugModule.getDependencies(controller),
});
}
controllers.push(...debuggedControllers);
break;
case MODULE_METADATA.EXPORTS:
const baseExports = DebugModule.getExports(app);
exports.push(
...baseExports.map((exp) => ({
name: exp.name,
type: DebugModule.exportType(exp),
})),
);
break;
}
}
debuggedTree.push({
name: app.name,
imports,
providers,
controllers,
exports,
});
return debuggedTree;
}

private static getImports(app: Type<any>): Array<Type<any>> {
return Reflect.getMetadata(MODULE_METADATA.IMPORTS, app);
}

private static getController(app: Type<any>): Array<Type<any>> {
return Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, app);
}

private static getProviders(
app: Type<any>,
): (DebuggedProvider & { type: ProviderType })[] {
const baseProviders = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, app);
const debuggedProviders: (DebuggedProvider & {
type: ProviderType;
})[] = [];
for (const provider of baseProviders) {
let dependencies: () => any[];
// regular providers
if (!DebugModule.isCustomProvider(provider)) {
debuggedProviders.push({
name: provider.name,
dependencies: DebugModule.getDependencies(provider),
type: 'class',
});
// custom providers
} else {
// set provide defaults
const newProvider: DebuggedProvider & {
type: ProviderType;
} = {
name: DebugModule.getProviderName(provider.provide),
dependencies: [],
type: 'class',
};
if (provider.useValue) {
newProvider.type = 'value';
dependencies = () => [];
} else if (provider.useFactory) {
newProvider.type = 'factory';
dependencies = () => provider.inject.map(DebugModule.getProviderName);
} else {
newProvider.type = 'class';
dependencies = () =>
DebugModule.getDependencies(
provider.useClass || provider.useExisting,
);
}
newProvider.dependencies = dependencies();
debuggedProviders.push(newProvider);
}
}
return debuggedProviders;
}

private static getExports(app: Type<any>): Array<Type<any>> {
return Reflect.getMetadata(MODULE_METADATA.EXPORTS, app);
}

private static getDependencies(classObj: Type<any>): Array<string> {
const retDeps = [];
const typedDeps =
(Reflect.getMetadata('design:paramtypes', classObj) as Array<
Type<any>
>) || [];
for (const dep of typedDeps) {
retDeps.push(dep.name);
}
const selfDeps =
(Reflect.getMetadata('self:paramtypes', classObj) as [
{ index: number; param: string },
]) || [];
for (const selfDep of selfDeps) {
retDeps[selfDep.index] = selfDep.param;
}
return retDeps;
}

private static getProviderName(
provider: string | symbol | Type<any>,
): string {
return typeof provider === 'function' ? provider.name : provider.toString();
}

private static isCustomProvider(
provider: CustomProvider | Type<any>,
): provider is CustomProvider {
return (provider as any).provide;
}

private static exportType(classObj: Type<any>): 'module' | 'provider' {
let isModule = false;
for (const key of Object.keys(MODULE_METADATA)) {
if (Reflect.getMetadata(MODULE_METADATA[key], classObj)) {
isModule = true;
}
}
return isModule ? 'module' : 'provider';
}
}
88 changes: 88 additions & 0 deletions lib/exploration.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { INestApplicationContext, HttpModule } from '@nestjs/common';
import { NestContainer } from '@nestjs/core';
import { InternalCoreModule } from '@nestjs/core/injector/internal-core-module';
import { Module as NestModule } from '@nestjs/core/injector/module';
import { SpelunkedTree } from './spelunker.interface';

export class ExplorationModule {
static explore(app: INestApplicationContext): SpelunkedTree[] {
const dependencyMap = [];
const modulesArray = Array.from(
((app as any).container as NestContainer).getModules().values(),
);
modulesArray
.filter(
(module) =>
module.metatype.name !== InternalCoreModule.name &&
module.metatype.name !== HttpModule.name,
)
.forEach((module) => {
dependencyMap.push({
name: module.metatype.name,
imports: this.getImports(module),
providers: this.getProviders(module),
controllers: this.getControllers(module),
exports: this.getExports(module),
});
});
return dependencyMap;
}

private static getImports(module: NestModule): string[] {
return Array.from(module.imports)
.filter((module) => module.metatype.name !== InternalCoreModule.name)
.map((module) => module.metatype.name);
}

private static getProviders(module: NestModule): any {
const providerList = {};
const providerNames = Array.from(module.providers.keys()).filter(
(provider) =>
provider !== module.metatype.name &&
provider !== 'ModuleRef' &&
provider !== 'ApplicationConfig',
);
providerNames.map((prov) => {
const provider = module.providers.get(prov);
const metatype = provider.metatype;
const name = (metatype && metatype.name) || 'useValue';
let provided = {};
switch (name) {
case 'useValue':
provided = {
method: 'value',
};
break;
case 'useClass':
provided = {
method: 'class',
};
break;
case 'useFactory':
provided = {
method: 'factory',
injections: provider.inject,
};
break;
default:
provided = {
method: 'standard',
};
}
providerList[prov] = provided;
});
return providerList;
}

private static getControllers(module: NestModule): string[] {
return Array.from(module.controllers.values()).map(
(controller) => controller.metatype.name,
);
}

private static getExports(module: NestModule): string[] {
return Array.from(module.exports).map((exportValue) =>
exportValue.toString(),
);
}
}
31 changes: 31 additions & 0 deletions lib/spelunker.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Type } from '@nestjs/common';

export interface SpelunkedTree {
name: string;
imports: string[];
Expand All @@ -10,3 +12,32 @@ interface SpelunkedProvider {
method: 'value' | 'factory' | 'class' | 'standard';
injections?: string[];
}

export type ProviderType = 'value' | 'factory' | 'class';

export interface DebuggedTree {
name: string;
imports: string[];
providers: Array<DebuggedProvider & { type: ProviderType }>;
controllers: DebuggedProvider[];
exports: DebuggedExports[];
}

export interface DebuggedProvider {
name: string;
dependencies: string[];
}

export interface DebuggedExports {
name: string;
type: 'provider' | 'module';
}

export interface CustomProvider {
provide: Type<any> | string | symbol;
useClass?: Type<any>;
useValue?: any;
useFactory?: (...args: any[]) => any;
useExisting?: Type<any>;
inject?: any[];
}
88 changes: 7 additions & 81 deletions lib/spelunker.module.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,14 @@
import { INestApplicationContext, HttpModule } from '@nestjs/common';
import { NestContainer } from '@nestjs/core';
import { InternalCoreModule } from '@nestjs/core/injector/internal-core-module';
import { Module as NestModule } from '@nestjs/core/injector/module';
import { SpelunkedTree } from './spelunker.interface';
import { INestApplicationContext, Type } from '@nestjs/common';
import { DebugModule } from './debug.module';
import { ExplorationModule } from './exploration.module';
import { SpelunkedTree, DebuggedTree } from './spelunker.interface';

export class SpelunkerModule {
static explore(app: INestApplicationContext): SpelunkedTree[] {
const dependencyMap = [];
const modulesArray = Array.from(
((app as any).container as NestContainer).getModules().values(),
);
modulesArray
.filter(
(module) =>
module.metatype.name !== InternalCoreModule.name &&
module.metatype.name !== HttpModule.name,
)
.forEach((module) => {
dependencyMap.push({
name: module.metatype.name,
imports: this.getImports(module),
providers: this.getProviders(module),
controllers: this.getControllers(module),
exports: this.getExports(module),
});
});
return dependencyMap;
return ExplorationModule.explore(app);
}

private static getImports(module: NestModule): string[] {
return Array.from(module.imports)
.filter((module) => module.metatype.name !== InternalCoreModule.name)
.map((module) => module.metatype.name);
}

private static getProviders(module: NestModule): any {
const providerList = {};
const providerNames = Array.from(module.providers.keys()).filter(
(provider) =>
provider !== module.metatype.name &&
provider !== 'ModuleRef' &&
provider !== 'ApplicationConfig',
);
providerNames.map((prov) => {
const provider = module.providers.get(prov);
const metatype = provider.metatype;
const name = (metatype && metatype.name) || 'useValue';
let provided = {};
switch (name) {
case 'useValue':
provided = {
method: 'value',
};
break;
case 'useClass':
provided = {
method: 'class',
};
break;
case 'useFactory':
provided = {
method: 'factory',
injections: provider.inject,
};
break;
default:
provided = {
method: 'standard',
};
}
providerList[prov] = provided;
});
return providerList;
}

private static getControllers(module: NestModule): string[] {
return Array.from(module.controllers.values()).map(
(controller) => controller.metatype.name,
);
}

private static getExports(module: NestModule): string[] {
return Array.from(module.exports).map((exportValue) =>
exportValue.toString(),
);
static debug(mod: Type<any>): DebuggedTree[] {
return DebugModule.debug(mod);
}
}

0 comments on commit 2d8510c

Please sign in to comment.