import { forwardRef, Module } from '@nestjs/common'; import { DiscoveryModule } from '@nestjs/core'; import { AllInOneModule } from '../all-in-one.module'; import { AppModule } from '../app.module'; import { ThrottlerStorageModule } from '../common/throttler/throttler-storage.module'; import { WorkerModule } from '../worker/worker.module'; import { WorkerRuntimeModule } from '../worker/worker-runtime.module'; import { isHttpRole, rootModuleFor } from './root-module'; const importsOf = (module: object): unknown[] => (Reflect.getMetadata('imports', module) as unknown[] | undefined) ?? []; /** * Every module class reachable from `root` through imports - including * the imports of dynamic modules (also async ones like * ConfigModule.forRoot) and forwardRef()s. */ async function reachableModules(root: unknown): Promise> { const seen = new Set(); async function visit(entry: unknown): Promise { const resolved: unknown = await entry; // A circular file import can leave an undefined slot behind. if (typeof resolved !== 'object' && typeof resolved !== 'function') { return; } if (resolved === null) { return; } if ('forwardRef' in resolved) { return visit((resolved as { forwardRef: () => unknown }).forwardRef()); } if ('module' in resolved) { const dynamic = resolved as { module: unknown; imports?: unknown[] }; await visit(dynamic.module); for (const imported of dynamic.imports ?? []) { await visit(imported); } return; } if (seen.has(resolved)) { return; } seen.add(resolved); for (const imported of importsOf(resolved)) { await visit(imported); } } await visit(root); return seen; } // The API role never starts background loops: only the worker and "all" // (local development and tests) load the worker runtime. describe('rootModuleFor', () => { it('the api module never imports the worker runtime, not even transitively', async () => { const reachable = await reachableModules(AppModule); expect(reachable).not.toContain(WorkerRuntimeModule); // The walk really goes deep: into feature modules and dynamic modules. expect(reachable).toContain(DiscoveryModule); expect(reachable).toContain(ThrottlerStorageModule); }); it('api starts the API module alone', () => { expect(rootModuleFor('api')).toBe(AppModule); }); it('all adds the worker runtime to the API', () => { expect(rootModuleFor('all')).toBe(AllInOneModule); expect(importsOf(AllInOneModule)).toEqual([AppModule, WorkerRuntimeModule]); }); it('the worker runs the runtime without the HTTP API', async () => { const reachable = await reachableModules(WorkerModule); expect(reachable).toContain(WorkerRuntimeModule); expect(reachable).not.toContain(AppModule); }); }); describe('reachableModules', () => { @Module({ imports: [WorkerRuntimeModule] }) class InnerModule {} @Module({ imports: [forwardRef(() => InnerModule)] }) class ViaForwardRefModule {} @Module({ imports: [Promise.resolve({ module: class {}, imports: [InnerModule] })], }) class ViaAsyncDynamicModule {} it.each([ ['a plain import chain', InnerModule], ['a forwardRef', ViaForwardRefModule], ['the imports of an async dynamic module', ViaAsyncDynamicModule], ])('finds the worker runtime behind %s', async (_case, root) => { expect(await reachableModules(root)).toContain(WorkerRuntimeModule); }); }); describe('isHttpRole', () => { it('admits exactly the roles dist/main.js starts', () => { expect(isHttpRole('api')).toBe(true); expect(isHttpRole('all')).toBe(true); expect(isHttpRole('worker')).toBe(false); expect(isHttpRole('job')).toBe(false); }); });