From 245ccd1249cf4e46e92ddadfc4c3ddabd5a6cb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sun, 29 May 2022 16:08:15 +0200 Subject: [PATCH 01/27] feat(core): repl --- integration/repl/e2e/repl.spec.ts | 106 ++++++++++ integration/repl/src/app.module.ts | 7 + .../repl/src/users/dto/create-user.dto.ts | 1 + .../repl/src/users/dto/update-user.dto.ts | 4 + .../repl/src/users/entities/user.entity.ts | 1 + .../repl/src/users/users.controller.spec.ts | 20 ++ .../repl/src/users/users.controller.ts | 34 ++++ integration/repl/src/users/users.module.ts | 16 ++ .../repl/src/users/users.repository.ts | 8 + .../repl/src/users/users.service.spec.ts | 18 ++ integration/repl/src/users/users.service.ts | 32 +++ integration/repl/tsconfig.json | 22 +++ packages/core/index.ts | 1 + packages/core/repl/constants.ts | 1 + packages/core/repl/index.ts | 1 + packages/core/repl/repl-context.ts | 167 ++++++++++++++++ packages/core/repl/repl-logger.ts | 18 ++ packages/core/repl/repl.ts | 31 +++ packages/core/test/repl/repl-context.spec.ts | 185 ++++++++++++++++++ 19 files changed, 673 insertions(+) create mode 100644 integration/repl/e2e/repl.spec.ts create mode 100644 integration/repl/src/app.module.ts create mode 100644 integration/repl/src/users/dto/create-user.dto.ts create mode 100644 integration/repl/src/users/dto/update-user.dto.ts create mode 100644 integration/repl/src/users/entities/user.entity.ts create mode 100644 integration/repl/src/users/users.controller.spec.ts create mode 100644 integration/repl/src/users/users.controller.ts create mode 100644 integration/repl/src/users/users.module.ts create mode 100644 integration/repl/src/users/users.repository.ts create mode 100644 integration/repl/src/users/users.service.spec.ts create mode 100644 integration/repl/src/users/users.service.ts create mode 100644 integration/repl/tsconfig.json create mode 100644 packages/core/repl/constants.ts create mode 100644 packages/core/repl/index.ts create mode 100644 packages/core/repl/repl-context.ts create mode 100644 packages/core/repl/repl-logger.ts create mode 100644 packages/core/repl/repl.ts create mode 100644 packages/core/test/repl/repl-context.spec.ts diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts new file mode 100644 index 00000000000..318b16f7a4b --- /dev/null +++ b/integration/repl/e2e/repl.spec.ts @@ -0,0 +1,106 @@ +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { repl } from '@nestjs/core'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { AppModule } from '../src/app.module'; +import { UsersModule } from '../src/users/users.module'; + +const prompt = '\u001b[1G\u001b[0J\u001b[32m>\u001b[0m \u001b[3G'; + +describe('REPL', () => { + beforeEach(() => { + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => { + sinon.restore(); + delete globalThis[AppModule.name]; + delete globalThis[UsersModule.name]; + }); + + it('get()', async () => { + const server = await repl(AppModule); + + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'get(UsersService)'); + + expect(outputText).to.equal( + `UsersService { usersRepository: UsersRepository {} } +${prompt}`, + ); + + outputText = ''; + server.emit('line', 'get(UsersService).findAll()'); + + expect(outputText).to + .equal(`\u001b[32m'This action returns all users'\u001b[39m +${prompt}`); + + outputText = ''; + server.emit('line', 'get(UsersRepository)'); + + expect(outputText).to.equal(`UsersRepository {} +${prompt}`); + }); + + it('debug()', async () => { + const server = await repl(AppModule); + + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'debug(UsersModule)'); + + expect(outputText).to.equal( + ` +UsersModule: + - controllers: + ◻ UsersController + - providers: + ◻ UsersService + ◻ UsersRepository + +${prompt}`, + ); + }); + + it('methods()', async () => { + const server = await repl(AppModule); + + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'methods(UsersRepository)'); + + expect(outputText).to.equal( + ` +Methods: + ◻ find + +${prompt}`, + ); + + outputText = ''; + server.emit('line', 'methods(UsersService)'); + + expect(outputText).to.equal( + ` +Methods: + ◻ create + ◻ findAll + ◻ findOne + ◻ update + ◻ remove + +${prompt}`, + ); + }); +}); diff --git a/integration/repl/src/app.module.ts b/integration/repl/src/app.module.ts new file mode 100644 index 00000000000..867c4b1b3c5 --- /dev/null +++ b/integration/repl/src/app.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { UsersModule } from './users/users.module'; + +@Module({ + imports: [UsersModule], +}) +export class AppModule {} diff --git a/integration/repl/src/users/dto/create-user.dto.ts b/integration/repl/src/users/dto/create-user.dto.ts new file mode 100644 index 00000000000..0311be1384d --- /dev/null +++ b/integration/repl/src/users/dto/create-user.dto.ts @@ -0,0 +1 @@ +export class CreateUserDto {} diff --git a/integration/repl/src/users/dto/update-user.dto.ts b/integration/repl/src/users/dto/update-user.dto.ts new file mode 100644 index 00000000000..dfd37fb1edb --- /dev/null +++ b/integration/repl/src/users/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/integration/repl/src/users/entities/user.entity.ts b/integration/repl/src/users/entities/user.entity.ts new file mode 100644 index 00000000000..4f82c14571c --- /dev/null +++ b/integration/repl/src/users/entities/user.entity.ts @@ -0,0 +1 @@ +export class User {} diff --git a/integration/repl/src/users/users.controller.spec.ts b/integration/repl/src/users/users.controller.spec.ts new file mode 100644 index 00000000000..a76d3103442 --- /dev/null +++ b/integration/repl/src/users/users.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +describe('UsersController', () => { + let controller: UsersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [UsersService], + }).compile(); + + controller = module.get(UsersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/integration/repl/src/users/users.controller.ts b/integration/repl/src/users/users.controller.ts new file mode 100644 index 00000000000..3eca7ebdeed --- /dev/null +++ b/integration/repl/src/users/users.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; + +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + create(@Body() createUserDto: CreateUserDto) { + return this.usersService.create(createUserDto); + } + + @Get() + findAll() { + return this.usersService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.usersService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.usersService.update(+id, updateUserDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.usersService.remove(+id); + } +} diff --git a/integration/repl/src/users/users.module.ts b/integration/repl/src/users/users.module.ts new file mode 100644 index 00000000000..86d73a3f0e4 --- /dev/null +++ b/integration/repl/src/users/users.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersRepository } from './users.repository'; +import { UsersService } from './users.service'; + +@Module({ + controllers: [UsersController], + providers: [ + UsersService, + { + provide: UsersRepository.name, + useValue: new UsersRepository(), + }, + ], +}) +export class UsersModule {} diff --git a/integration/repl/src/users/users.repository.ts b/integration/repl/src/users/users.repository.ts new file mode 100644 index 00000000000..4a616658654 --- /dev/null +++ b/integration/repl/src/users/users.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UsersRepository { + async find() { + return [{ id: 1, email: 'test@nestjs.com' }]; + } +} diff --git a/integration/repl/src/users/users.service.spec.ts b/integration/repl/src/users/users.service.spec.ts new file mode 100644 index 00000000000..62815ba6412 --- /dev/null +++ b/integration/repl/src/users/users.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersService } from './users.service'; + +describe('UsersService', () => { + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UsersService], + }).compile(); + + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/integration/repl/src/users/users.service.ts b/integration/repl/src/users/users.service.ts new file mode 100644 index 00000000000..ab3191152d8 --- /dev/null +++ b/integration/repl/src/users/users.service.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UsersRepository } from './users.repository'; + +@Injectable() +export class UsersService { + constructor( + @Inject('UsersRepository') + private readonly usersRepository: UsersRepository, + ) {} + + create(createUserDto: CreateUserDto) { + return 'This action adds a new user'; + } + + findAll() { + return `This action returns all users`; + } + + findOne(id: number) { + return `This action returns a #${id} user`; + } + + update(id: number, updateUserDto: UpdateUserDto) { + return `This action updates a #${id} user`; + } + + remove(id: number) { + return `This action removes a #${id} user`; + } +} diff --git a/integration/repl/tsconfig.json b/integration/repl/tsconfig.json new file mode 100644 index 00000000000..ea15af490a5 --- /dev/null +++ b/integration/repl/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "allowJs": true, + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/packages/core/index.ts b/packages/core/index.ts index ebc30ca4ca7..1babf6c15f7 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -18,5 +18,6 @@ export * from './middleware'; export * from './nest-application'; export * from './nest-application-context'; export { NestFactory } from './nest-factory'; +export * from './repl'; export * from './router'; export * from './services'; diff --git a/packages/core/repl/constants.ts b/packages/core/repl/constants.ts new file mode 100644 index 00000000000..870e2ec5190 --- /dev/null +++ b/packages/core/repl/constants.ts @@ -0,0 +1 @@ +export const REPL_INITIALIZED_MESSAGE = 'REPL initialized'; diff --git a/packages/core/repl/index.ts b/packages/core/repl/index.ts new file mode 100644 index 00000000000..8a399bd09a5 --- /dev/null +++ b/packages/core/repl/index.ts @@ -0,0 +1 @@ +export * from './repl'; diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts new file mode 100644 index 00000000000..08d32190f18 --- /dev/null +++ b/packages/core/repl/repl-context.ts @@ -0,0 +1,167 @@ +import { + DynamicModule, + INestApplication, + InjectionToken, + Logger, + Type, +} from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ApplicationConfig } from '../application-config'; +import { ModuleRef, NestContainer } from '../injector'; +import { InternalCoreModule } from '../injector/internal-core-module'; +import { Module } from '../injector/module'; +import { MetadataScanner } from '../metadata-scanner'; + +type ModuleKey = string; +type ModuleDebugEntry = { + controllers: Record; + providers: Record; +}; + +export class ReplContext { + private debugRegistry: Record = {}; + private readonly container: NestContainer; + private readonly logger = new Logger(ReplContext.name); + private readonly metadataScanner = new MetadataScanner(); + + constructor(private readonly app: INestApplication) { + this.container = (app as any).container; + this.initialize(); + } + + $(token: string | symbol | Function | Type) { + return this.get(token); + } + + get(token: string | symbol | Function | Type) { + return this.app.get(token); + } + + resolve(token: string | symbol | Function | Type, contextId: any) { + return this.app.resolve(token, contextId); + } + + select(token: DynamicModule | Type) { + return this.app.select(token); + } + + debug(moduleCls?: Type | string) { + this.writeToStdout('\n'); + + if (moduleCls) { + const token = + typeof moduleCls === 'function' ? moduleCls.name : moduleCls; + const moduleEntry = this.debugRegistry[token]; + if (!moduleEntry) { + return this.logger.error( + `"${token}" has not been found in the modules registry`, + ); + } + this.printCtrlsAndProviders(token, moduleEntry); + } else { + Object.keys(this.debugRegistry).forEach(moduleKey => { + this.printCtrlsAndProviders(moduleKey, this.debugRegistry[moduleKey]); + }); + } + this.writeToStdout('\n'); + } + + methods(token: Type | string) { + const proto = + typeof token !== 'function' + ? Object.getPrototypeOf(this.app.get(token)) + : token?.prototype; + + const methods = new Set( + this.metadataScanner.getAllFilteredMethodNames(proto), + ); + + this.writeToStdout('\n'); + this.writeToStdout(`${clc.green('Methods')}: \n`); + methods.forEach(methodName => + this.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), + ); + this.writeToStdout('\n'); + } + + private initialize() { + const globalRef = globalThis; + const modules = this.container.getModules(); + + modules.forEach(moduleRef => { + let moduleName = moduleRef.metatype.name; + if (moduleName === InternalCoreModule.name) { + return; + } + if (globalRef[moduleName]) { + moduleName += ` (${moduleRef.token})`; + } + + this.introspectCollection(moduleRef, moduleName, 'providers'); + this.introspectCollection(moduleRef, moduleName, 'controllers'); + + globalRef[moduleName] = moduleRef.metatype; + }); + } + + private introspectCollection( + moduleRef: Module, + moduleKey: ModuleKey, + collection: keyof ModuleDebugEntry, + ) { + let moduleDebugEntry = {}; + moduleRef[collection].forEach(({ token }) => { + const stringifiedToken = this.stringifyToken(token); + if ( + stringifiedToken === ApplicationConfig.name || + stringifiedToken === moduleRef.metatype.name + ) { + return; + } + // For in REPL auto-complete functionality + globalThis[stringifiedToken] = token; + + if (stringifiedToken === ModuleRef.name) { + return; + } + moduleDebugEntry[stringifiedToken] = token; + }); + + this.debugRegistry[moduleKey] = { + ...this.debugRegistry?.[moduleKey], + [collection]: moduleDebugEntry, + }; + } + + private stringifyToken(token: unknown): string { + return typeof token !== 'string' + ? typeof token === 'function' + ? token.name + : token?.toString() + : token; + } + + private printCtrlsAndProviders( + moduleName: string, + moduleDebugEntry: ModuleDebugEntry, + ) { + const printCollection = (collection: keyof ModuleDebugEntry) => { + const collectionEntries = Object.keys(moduleDebugEntry[collection]); + if (collectionEntries.length <= 0) { + return; + } + this.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); + collectionEntries.forEach(provider => + this.writeToStdout(` ${clc.green('◻')} ${provider}\n`), + ); + }; + + this.writeToStdout(`${clc.green(moduleName)}: \n`); + printCollection('controllers'); + printCollection('providers'); + } + + private writeToStdout(text: string) { + process.stdout.write(text); + } +} diff --git a/packages/core/repl/repl-logger.ts b/packages/core/repl/repl-logger.ts new file mode 100644 index 00000000000..900ae580b62 --- /dev/null +++ b/packages/core/repl/repl-logger.ts @@ -0,0 +1,18 @@ +import { ConsoleLogger } from '@nestjs/common'; +import { NestApplication } from '../nest-application'; +import { RouterExplorer } from '../router/router-explorer'; +import { RoutesResolver } from '../router/routes-resolver'; + +export class ReplLogger extends ConsoleLogger { + private static readonly ignoredContexts = [ + RoutesResolver.name, + RouterExplorer.name, + NestApplication.name, + ]; + log(_message: any, context?: string) { + if (ReplLogger.ignoredContexts.includes(context)) { + return; + } + return super.log.apply(this, Array.from(arguments) as [any, string?]); + } +} diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts new file mode 100644 index 00000000000..e88d8bd5f43 --- /dev/null +++ b/packages/core/repl/repl.ts @@ -0,0 +1,31 @@ +import { Logger, Type } from '@nestjs/common'; +import * as _repl from 'repl'; +import { NestFactory } from '../nest-factory'; +import { REPL_INITIALIZED_MESSAGE } from './constants'; +import { ReplContext } from './repl-context'; +import { ReplLogger } from './repl-logger'; + +export async function repl(module: Type) { + const app = await NestFactory.create(module, { + abortOnError: false, + logger: new ReplLogger(), + }); + await app.init(); + + const replContext = new ReplContext(app); + Logger.log(REPL_INITIALIZED_MESSAGE); + + const replServer = _repl.start({ + prompt: '\x1b[32m>\x1b[0m ', + ignoreUndefined: true, + }); + + replServer.context.$ = replContext.$.bind(replContext); + replServer.context.get = replContext.get.bind(replContext); + replServer.context.resolve = replContext.resolve.bind(replContext); + replServer.context.select = replContext.select.bind(replContext); + replServer.context.debug = replContext.debug.bind(replContext); + replServer.context.methods = replContext.methods.bind(replContext); + + return replServer; +} diff --git a/packages/core/test/repl/repl-context.spec.ts b/packages/core/test/repl/repl-context.spec.ts new file mode 100644 index 00000000000..4446dda811a --- /dev/null +++ b/packages/core/test/repl/repl-context.spec.ts @@ -0,0 +1,185 @@ +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { NestContainer } from '../../injector/container'; +import { ReplContext } from '../../repl/repl-context'; + +describe('ReplContext', () => { + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + const aModuleRef = await container.addModule(class ModuleA {}, []); + const bModuleRef = await container.addModule(class ModuleB {}, []); + + container.addController(class ControllerA {}, aModuleRef.token); + container.addProvider(class ProviderA1 {}, aModuleRef.token); + container.addProvider(class ProviderA2 {}, aModuleRef.token); + + container.addProvider(class ProviderB1 {}, bModuleRef.token); + container.addProvider(class ProviderB2 {}, bModuleRef.token); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + describe('debug', () => { + it('should print all modules along with their controllers and providers', () => { + let outputText = ''; + + sinon + .stub(replContext as any, 'writeToStdout') + .callsFake(text => (outputText += text)); + replContext.debug(); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 +ModuleB: + - providers: + ◻ ProviderB1 + ◻ ProviderB2 + +`); + }); + + describe('when module passed as a class reference', () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext as any, 'writeToStdout') + .callsFake(text => (outputText += text)); + replContext.debug(class ModuleA {}); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + describe("when module passed as a string (module's key)", () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext as any, 'writeToStdout') + .callsFake(text => (outputText += text)); + replContext.debug('ModuleA'); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + }); + + describe('methods', () => { + describe('when token is a class reference', () => { + it('should print all class methods', () => { + class BaseService { + create() {} + } + class TestService extends BaseService { + findAll() {} + findOne() {} + } + + let outputText = ''; + + sinon + .stub(replContext as any, 'writeToStdout') + .callsFake(text => (outputText += text)); + replContext.methods(TestService); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + ◻ create + +`); + }); + }); + + describe('when token is a string', () => { + it('should grab provider from the container and print its all methods', () => { + class ProviderA1 { + findAll() {} + findOne() {} + } + let outputText = ''; + + sinon + .stub(replContext as any, 'writeToStdout') + .callsFake(text => (outputText += text)); + + mockApp.get.callsFake(() => new ProviderA1()); + replContext.methods('ProviderA1'); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + +`); + }); + }); + }); + + describe('get', () => { + it('should pass arguments down to the application context', () => { + const token = 'test'; + replContext.get(token); + expect(mockApp.get.calledWith(token)).to.be.true; + }); + }); + describe('resolve', () => { + it('should pass arguments down to the application context', async () => { + const token = 'test'; + const contextId = {}; + + await replContext.resolve(token, contextId); + expect(mockApp.resolve.calledWith(token, contextId)).to.be.true; + }); + }); + describe('select', () => { + it('should pass arguments down to the application context', () => { + const moduleCls = class TestModule {}; + replContext.select(moduleCls); + expect(mockApp.select.calledWith(moduleCls)).to.be.true; + }); + }); +}); From bae30b23ce1e4264b6ba00fcc341471add16ce78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sun, 29 May 2022 16:20:59 +0200 Subject: [PATCH 02/27] test(): remove unnecessary files --- .../repl/src/users/users.controller.spec.ts | 20 ------------------- .../repl/src/users/users.service.spec.ts | 18 ----------------- 2 files changed, 38 deletions(-) delete mode 100644 integration/repl/src/users/users.controller.spec.ts delete mode 100644 integration/repl/src/users/users.service.spec.ts diff --git a/integration/repl/src/users/users.controller.spec.ts b/integration/repl/src/users/users.controller.spec.ts deleted file mode 100644 index a76d3103442..00000000000 --- a/integration/repl/src/users/users.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; - -describe('UsersController', () => { - let controller: UsersController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UsersController], - providers: [UsersService], - }).compile(); - - controller = module.get(UsersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/integration/repl/src/users/users.service.spec.ts b/integration/repl/src/users/users.service.spec.ts deleted file mode 100644 index 62815ba6412..00000000000 --- a/integration/repl/src/users/users.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from './users.service'; - -describe('UsersService', () => { - let service: UsersService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UsersService], - }).compile(); - - service = module.get(UsersService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); From 21d6dd1376c321e3f9f0472483ddf91c7339cbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sun, 29 May 2022 16:23:46 +0200 Subject: [PATCH 03/27] lint(core): address linter errors --- packages/core/repl/repl-context.ts | 2 +- packages/core/repl/repl-logger.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index 08d32190f18..1be5b25e64e 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -109,7 +109,7 @@ export class ReplContext { moduleKey: ModuleKey, collection: keyof ModuleDebugEntry, ) { - let moduleDebugEntry = {}; + const moduleDebugEntry = {}; moduleRef[collection].forEach(({ token }) => { const stringifiedToken = this.stringifyToken(token); if ( diff --git a/packages/core/repl/repl-logger.ts b/packages/core/repl/repl-logger.ts index 900ae580b62..e6195269ffe 100644 --- a/packages/core/repl/repl-logger.ts +++ b/packages/core/repl/repl-logger.ts @@ -13,6 +13,7 @@ export class ReplLogger extends ConsoleLogger { if (ReplLogger.ignoredContexts.includes(context)) { return; } + // eslint-disable-next-line return super.log.apply(this, Array.from(arguments) as [any, string?]); } } From a5ecaebda11f18e83b247920811480aaca61b0c6 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 02:04:47 -0400 Subject: [PATCH 04/27] feat(common): add bold coloring to cli colors utils --- packages/common/utils/cli-colors.util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/utils/cli-colors.util.ts b/packages/common/utils/cli-colors.util.ts index 8efd6167054..738fbc6d337 100644 --- a/packages/common/utils/cli-colors.util.ts +++ b/packages/common/utils/cli-colors.util.ts @@ -5,6 +5,7 @@ const colorIfAllowed = (colorFn: ColorTextFn) => (text: string) => isColorAllowed() ? colorFn(text) : text; export const clc = { + bold: colorIfAllowed((text: string) => `\x1B[1m${text}\x1B[0m`), green: colorIfAllowed((text: string) => `\x1B[32m${text}\x1B[39m`), yellow: colorIfAllowed((text: string) => `\x1B[33m${text}\x1B[39m`), red: colorIfAllowed((text: string) => `\x1B[31m${text}\x1B[39m`), From f37e5d5cf863d5970fb28bc9b1c0517ce89a85bc Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 02:05:44 -0400 Subject: [PATCH 05/27] feat(core): add help messages to built-in repl functions --- packages/core/repl/native-functions.ts | 129 +++++++++++++++++++++++++ packages/core/repl/repl-context.ts | 124 +++++++----------------- packages/core/repl/repl-function.ts | 36 +++++++ packages/core/repl/repl.interfaces.ts | 21 ++++ packages/core/repl/repl.ts | 54 +++++++++-- 5 files changed, 269 insertions(+), 95 deletions(-) create mode 100644 packages/core/repl/native-functions.ts create mode 100644 packages/core/repl/repl-function.ts create mode 100644 packages/core/repl/repl.interfaces.ts diff --git a/packages/core/repl/native-functions.ts b/packages/core/repl/native-functions.ts new file mode 100644 index 00000000000..498f0a0c038 --- /dev/null +++ b/packages/core/repl/native-functions.ts @@ -0,0 +1,129 @@ +import { DynamicModule, INestApplicationContext, Type } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { MetadataScanner } from '../metadata-scanner'; +import { ReplFunction } from './repl-function'; +import type { ModuleDebugEntry } from './repl-context'; +import type { ReplFnDefinition } from './repl.interfaces'; + +export class GetReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'get', + signature: '(token: InjectionToken) => any', + description: + 'Retrieves an instance of either injectable or controller, otherwise, throws exception.', + aliases: ['$'], + }; + + action(token: string | symbol | Function | Type): any { + this.ctx.app.get(token); + } +} + +export class ResolveReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'resolve', + description: + 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception', + signature: '(token: InjectionToken, contextId: any) => Promise', + }; + + action( + token: string | symbol | Function | Type, + contextId: any, + ): Promise { + return this.ctx.app.resolve(token, contextId); + } +} + +export class SelectReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'select', + description: + 'Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.', + signature: '(token: DynamicModule | ClassRef) => INestApplicationContext', + }; + + action(token: DynamicModule | Type): INestApplicationContext { + return this.ctx.app.select(token); + } +} + +export class DebugReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'debug', + description: '', + signature: '(moduleCls?: ClassRef | string) => void', + }; + + action(moduleCls?: Type | string): void { + this.ctx.writeToStdout('\n'); + + if (moduleCls) { + const token = + typeof moduleCls === 'function' ? moduleCls.name : moduleCls; + const moduleEntry = this.ctx.debugRegistry[token]; + if (!moduleEntry) { + return this.logger.error( + `"${token}" has not been found in the modules registry`, + ); + } + this.printCtrlsAndProviders(token, moduleEntry); + } else { + Object.keys(this.ctx.debugRegistry).forEach(moduleKey => { + this.printCtrlsAndProviders( + moduleKey, + this.ctx.debugRegistry[moduleKey], + ); + }); + } + this.ctx.writeToStdout('\n'); + } + + private printCtrlsAndProviders( + moduleName: string, + moduleDebugEntry: ModuleDebugEntry, + ) { + const printCollection = (collection: keyof ModuleDebugEntry) => { + const collectionEntries = Object.keys(moduleDebugEntry[collection]); + if (collectionEntries.length <= 0) { + return; + } + this.ctx.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); + collectionEntries.forEach(provider => + this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), + ); + }; + + this.ctx.writeToStdout(`${clc.green(moduleName)}: \n`); + printCollection('controllers'); + printCollection('providers'); + } +} + +export class MethodsReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'methods', + description: 'Display all public methods available on a given provider.', + signature: '(token: ClassRef | string) => void', + }; + + private readonly metadataScanner = new MetadataScanner(); + + action(token: Type | string): void { + const proto = + typeof token !== 'function' + ? Object.getPrototypeOf(this.ctx.app.get(token)) + : token?.prototype; + + const methods = new Set( + this.metadataScanner.getAllFilteredMethodNames(proto), + ); + + this.ctx.writeToStdout('\n'); + this.ctx.writeToStdout(`${clc.green('Methods')}: \n`); + methods.forEach(methodName => + this.ctx.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), + ); + this.ctx.writeToStdout('\n'); + } +} diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index 1be5b25e64e..c4ea975cff1 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -1,87 +1,37 @@ -import { - DynamicModule, - INestApplication, - InjectionToken, - Logger, - Type, -} from '@nestjs/common'; -import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { INestApplication, InjectionToken, Logger } from '@nestjs/common'; import { ApplicationConfig } from '../application-config'; import { ModuleRef, NestContainer } from '../injector'; import { InternalCoreModule } from '../injector/internal-core-module'; import { Module } from '../injector/module'; -import { MetadataScanner } from '../metadata-scanner'; +import { + DebugReplFn, + GetReplFn, + MethodsReplFn, + ResolveReplFn, + SelectReplFn, +} from './native-functions'; +import type { ReplFunctionClass } from './repl.interfaces'; type ModuleKey = string; -type ModuleDebugEntry = { +export type ModuleDebugEntry = { controllers: Record; providers: Record; }; export class ReplContext { - private debugRegistry: Record = {}; - private readonly container: NestContainer; - private readonly logger = new Logger(ReplContext.name); - private readonly metadataScanner = new MetadataScanner(); + public nativeFunctions: InstanceType[]; - constructor(private readonly app: INestApplication) { + public debugRegistry: Record = {}; + public readonly container: NestContainer; + public readonly logger = new Logger(ReplContext.name); + + constructor( + public readonly app: INestApplication, + nativeFunctionsClassRefs?: ReplFunctionClass[], + ) { this.container = (app as any).container; this.initialize(); - } - - $(token: string | symbol | Function | Type) { - return this.get(token); - } - - get(token: string | symbol | Function | Type) { - return this.app.get(token); - } - - resolve(token: string | symbol | Function | Type, contextId: any) { - return this.app.resolve(token, contextId); - } - - select(token: DynamicModule | Type) { - return this.app.select(token); - } - - debug(moduleCls?: Type | string) { - this.writeToStdout('\n'); - - if (moduleCls) { - const token = - typeof moduleCls === 'function' ? moduleCls.name : moduleCls; - const moduleEntry = this.debugRegistry[token]; - if (!moduleEntry) { - return this.logger.error( - `"${token}" has not been found in the modules registry`, - ); - } - this.printCtrlsAndProviders(token, moduleEntry); - } else { - Object.keys(this.debugRegistry).forEach(moduleKey => { - this.printCtrlsAndProviders(moduleKey, this.debugRegistry[moduleKey]); - }); - } - this.writeToStdout('\n'); - } - - methods(token: Type | string) { - const proto = - typeof token !== 'function' - ? Object.getPrototypeOf(this.app.get(token)) - : token?.prototype; - - const methods = new Set( - this.metadataScanner.getAllFilteredMethodNames(proto), - ); - - this.writeToStdout('\n'); - this.writeToStdout(`${clc.green('Methods')}: \n`); - methods.forEach(methodName => - this.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), - ); - this.writeToStdout('\n'); + this.initializeNativeFunctions(nativeFunctionsClassRefs || []); } private initialize() { @@ -141,27 +91,23 @@ export class ReplContext { : token; } - private printCtrlsAndProviders( - moduleName: string, - moduleDebugEntry: ModuleDebugEntry, - ) { - const printCollection = (collection: keyof ModuleDebugEntry) => { - const collectionEntries = Object.keys(moduleDebugEntry[collection]); - if (collectionEntries.length <= 0) { - return; - } - this.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); - collectionEntries.forEach(provider => - this.writeToStdout(` ${clc.green('◻')} ${provider}\n`), - ); - }; - - this.writeToStdout(`${clc.green(moduleName)}: \n`); - printCollection('controllers'); - printCollection('providers'); + private initializeNativeFunctions( + nativeFunctionsClassRefs: ReplFunctionClass[], + ): void { + const builtInFunctionsClassRefs: ReplFunctionClass[] = [ + GetReplFn, + ResolveReplFn, + SelectReplFn, + DebugReplFn, + MethodsReplFn, + ]; + + this.nativeFunctions = builtInFunctionsClassRefs + .concat(nativeFunctionsClassRefs) + .map(NativeFn => new NativeFn(this)); } - private writeToStdout(text: string) { + public writeToStdout(text: string) { process.stdout.write(text); } } diff --git a/packages/core/repl/repl-function.ts b/packages/core/repl/repl-function.ts new file mode 100644 index 00000000000..aa90972c853 --- /dev/null +++ b/packages/core/repl/repl-function.ts @@ -0,0 +1,36 @@ +import { Logger } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplContext } from './repl-context'; +import type { ReplFnDefinition } from './repl.interfaces'; + +export abstract class ReplFunction< + ActionParams extends Array = Array, + ActionReturn = any, +> { + /** Metadata that describes the built-in function itself. */ + public abstract fnDefinition: ReplFnDefinition; + + protected readonly logger: Logger; + + constructor(protected readonly ctx: ReplContext) { + this.logger = ctx.logger; + } + + /** + * Method called when the function is invoked from the REPL by the user. + */ + abstract action(...args: ActionParams): ActionReturn; + + /** + * @returns A message displayed by calling `.help` + */ + public makeHelpMessage(): string { + const { description, name, signature } = this.fnDefinition; + + const fnSignatureWithName = `${name}${signature}`; + + return `${clc.yellow(description)}\n${clc.magentaBright( + 'Interface:', + )} ${clc.bold(fnSignatureWithName)}\n`; + } +} diff --git a/packages/core/repl/repl.interfaces.ts b/packages/core/repl/repl.interfaces.ts new file mode 100644 index 00000000000..ad4b8fd646c --- /dev/null +++ b/packages/core/repl/repl.interfaces.ts @@ -0,0 +1,21 @@ +import type { ReplContext } from './repl-context'; +import type { ReplFunction } from './repl-function'; + +export type ReplFnDefinition = { + /** Function's name. Note that this should be a valid JavaScript function name. */ + name: string; + + /** Alternative names to the function. */ + aliases?: ReplFnDefinition['name'][]; + + /** Function's description to display when `.help` is entered. */ + description: string; + + /** + * Function's signature following TypeScript _function type expression_ syntax. + * @example '(token: InjectionToken) => any' + */ + signature: string; +}; + +export type ReplFunctionClass = new (replContext: ReplContext) => ReplFunction; diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts index e88d8bd5f43..a41a25af5a9 100644 --- a/packages/core/repl/repl.ts +++ b/packages/core/repl/repl.ts @@ -3,7 +3,54 @@ import * as _repl from 'repl'; import { NestFactory } from '../nest-factory'; import { REPL_INITIALIZED_MESSAGE } from './constants'; import { ReplContext } from './repl-context'; +import { ReplFunction } from './repl-function'; import { ReplLogger } from './repl-logger'; +import { ReplFunctionClass } from './repl.interfaces'; + +function loadNativeFunctionsIntoContext( + replServerContext: _repl.REPLServer['context'], + replContext: ReplContext, +) { + const registerFunction = ( + nativeFunction: InstanceType, + ): void => { + // Bind the method to REPL's context: + replServerContext[nativeFunction.fnDefinition.name] = + nativeFunction.action.bind(nativeFunction); + + // Load the help trigger as a `help` getter on each native function: + const functionBoundRef: ReplFunction['action'] = + replServerContext[nativeFunction.fnDefinition.name]; + Object.defineProperty(functionBoundRef, 'help', { + enumerable: false, + configurable: false, + get: () => + // Dynamically builds the help message as will unlikely to be called + // several times. + replContext.writeToStdout(nativeFunction.makeHelpMessage()), + }); + }; + + const aliasesNativeFunctions = replContext.nativeFunctions + .filter(nativeFunction => nativeFunction.fnDefinition.aliases) + .flatMap(nativeFunction => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + nativeFunction.fnDefinition.aliases!.map(aliasFnName => { + const aliasNativeFunction: InstanceType = + Object.create(nativeFunction); + aliasNativeFunction.fnDefinition = { + name: aliasFnName, + description: aliasNativeFunction.fnDefinition.description, + signature: aliasNativeFunction.fnDefinition.signature, + }; + + return aliasNativeFunction; + }), + ); + + replContext.nativeFunctions.push(...aliasesNativeFunctions); + replContext.nativeFunctions.forEach(registerFunction); +} export async function repl(module: Type) { const app = await NestFactory.create(module, { @@ -20,12 +67,7 @@ export async function repl(module: Type) { ignoreUndefined: true, }); - replServer.context.$ = replContext.$.bind(replContext); - replServer.context.get = replContext.get.bind(replContext); - replServer.context.resolve = replContext.resolve.bind(replContext); - replServer.context.select = replContext.select.bind(replContext); - replServer.context.debug = replContext.debug.bind(replContext); - replServer.context.methods = replContext.methods.bind(replContext); + loadNativeFunctionsIntoContext(replServer.context, replContext); return replServer; } From 1b95a46a629213834821be6c84d5c8abf738f766 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 02:07:07 -0400 Subject: [PATCH 06/27] feat(core): add `help` native function to repl --- packages/core/repl/native-functions.ts | 27 ++++++++++++++++++++++++++ packages/core/repl/repl-context.ts | 2 ++ 2 files changed, 29 insertions(+) diff --git a/packages/core/repl/native-functions.ts b/packages/core/repl/native-functions.ts index 498f0a0c038..a7ddb757d66 100644 --- a/packages/core/repl/native-functions.ts +++ b/packages/core/repl/native-functions.ts @@ -5,6 +5,33 @@ import { ReplFunction } from './repl-function'; import type { ModuleDebugEntry } from './repl-context'; import type { ReplFnDefinition } from './repl.interfaces'; +export class HelpReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'help', + signature: '() => void', + description: 'Display all available REPL native functions.', + }; + + action(): void { + const buildHelpMessage = ({ name, description }: ReplFnDefinition) => + clc.cyanBright(name) + + (description ? ` ${clc.bold('-')} ${description}` : ''); + + const sortedNativeFunctions = this.ctx.nativeFunctions + .map(nativeFunction => nativeFunction.fnDefinition) + .sort((a, b) => (a.name < b.name ? -1 : 1)); + + this.ctx.writeToStdout( + `You can call ${clc.bold( + '.help', + )} on any function listed below (e.g.: ${clc.bold('help.help')}):\n\n` + + sortedNativeFunctions.map(buildHelpMessage).join('\n') + + // Without the following LF the last item won't be displayed + '\n', + ); + } +} + export class GetReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'get', diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index c4ea975cff1..8be2e600a3e 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -6,6 +6,7 @@ import { Module } from '../injector/module'; import { DebugReplFn, GetReplFn, + HelpReplFn, MethodsReplFn, ResolveReplFn, SelectReplFn, @@ -95,6 +96,7 @@ export class ReplContext { nativeFunctionsClassRefs: ReplFunctionClass[], ): void { const builtInFunctionsClassRefs: ReplFunctionClass[] = [ + HelpReplFn, GetReplFn, ResolveReplFn, SelectReplFn, From 8282d8f48f80e63891b51fd21624abe0793bc3e4 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 06:39:34 -0400 Subject: [PATCH 07/27] refactor(core): move each repl function to their own file --- packages/core/repl/native-functions.ts | 156 ------------------ .../repl/native-functions/debug-repl-fn.ts | 57 +++++++ .../core/repl/native-functions/get-relp-fn.ts | 17 ++ .../repl/native-functions/help-repl-fn.ts | 30 ++++ packages/core/repl/native-functions/index.ts | 6 + .../repl/native-functions/methods-repl-fn.ts | 33 ++++ .../repl/native-functions/resolve-repl-fn.ts | 19 +++ .../repl/native-functions/select-relp-fn.ts | 20 +++ 8 files changed, 182 insertions(+), 156 deletions(-) delete mode 100644 packages/core/repl/native-functions.ts create mode 100644 packages/core/repl/native-functions/debug-repl-fn.ts create mode 100644 packages/core/repl/native-functions/get-relp-fn.ts create mode 100644 packages/core/repl/native-functions/help-repl-fn.ts create mode 100644 packages/core/repl/native-functions/index.ts create mode 100644 packages/core/repl/native-functions/methods-repl-fn.ts create mode 100644 packages/core/repl/native-functions/resolve-repl-fn.ts create mode 100644 packages/core/repl/native-functions/select-relp-fn.ts diff --git a/packages/core/repl/native-functions.ts b/packages/core/repl/native-functions.ts deleted file mode 100644 index a7ddb757d66..00000000000 --- a/packages/core/repl/native-functions.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { DynamicModule, INestApplicationContext, Type } from '@nestjs/common'; -import { clc } from '@nestjs/common/utils/cli-colors.util'; -import { MetadataScanner } from '../metadata-scanner'; -import { ReplFunction } from './repl-function'; -import type { ModuleDebugEntry } from './repl-context'; -import type { ReplFnDefinition } from './repl.interfaces'; - -export class HelpReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'help', - signature: '() => void', - description: 'Display all available REPL native functions.', - }; - - action(): void { - const buildHelpMessage = ({ name, description }: ReplFnDefinition) => - clc.cyanBright(name) + - (description ? ` ${clc.bold('-')} ${description}` : ''); - - const sortedNativeFunctions = this.ctx.nativeFunctions - .map(nativeFunction => nativeFunction.fnDefinition) - .sort((a, b) => (a.name < b.name ? -1 : 1)); - - this.ctx.writeToStdout( - `You can call ${clc.bold( - '.help', - )} on any function listed below (e.g.: ${clc.bold('help.help')}):\n\n` + - sortedNativeFunctions.map(buildHelpMessage).join('\n') + - // Without the following LF the last item won't be displayed - '\n', - ); - } -} - -export class GetReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'get', - signature: '(token: InjectionToken) => any', - description: - 'Retrieves an instance of either injectable or controller, otherwise, throws exception.', - aliases: ['$'], - }; - - action(token: string | symbol | Function | Type): any { - this.ctx.app.get(token); - } -} - -export class ResolveReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'resolve', - description: - 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception', - signature: '(token: InjectionToken, contextId: any) => Promise', - }; - - action( - token: string | symbol | Function | Type, - contextId: any, - ): Promise { - return this.ctx.app.resolve(token, contextId); - } -} - -export class SelectReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'select', - description: - 'Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.', - signature: '(token: DynamicModule | ClassRef) => INestApplicationContext', - }; - - action(token: DynamicModule | Type): INestApplicationContext { - return this.ctx.app.select(token); - } -} - -export class DebugReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'debug', - description: '', - signature: '(moduleCls?: ClassRef | string) => void', - }; - - action(moduleCls?: Type | string): void { - this.ctx.writeToStdout('\n'); - - if (moduleCls) { - const token = - typeof moduleCls === 'function' ? moduleCls.name : moduleCls; - const moduleEntry = this.ctx.debugRegistry[token]; - if (!moduleEntry) { - return this.logger.error( - `"${token}" has not been found in the modules registry`, - ); - } - this.printCtrlsAndProviders(token, moduleEntry); - } else { - Object.keys(this.ctx.debugRegistry).forEach(moduleKey => { - this.printCtrlsAndProviders( - moduleKey, - this.ctx.debugRegistry[moduleKey], - ); - }); - } - this.ctx.writeToStdout('\n'); - } - - private printCtrlsAndProviders( - moduleName: string, - moduleDebugEntry: ModuleDebugEntry, - ) { - const printCollection = (collection: keyof ModuleDebugEntry) => { - const collectionEntries = Object.keys(moduleDebugEntry[collection]); - if (collectionEntries.length <= 0) { - return; - } - this.ctx.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); - collectionEntries.forEach(provider => - this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), - ); - }; - - this.ctx.writeToStdout(`${clc.green(moduleName)}: \n`); - printCollection('controllers'); - printCollection('providers'); - } -} - -export class MethodsReplFn extends ReplFunction { - public fnDefinition: ReplFnDefinition = { - name: 'methods', - description: 'Display all public methods available on a given provider.', - signature: '(token: ClassRef | string) => void', - }; - - private readonly metadataScanner = new MetadataScanner(); - - action(token: Type | string): void { - const proto = - typeof token !== 'function' - ? Object.getPrototypeOf(this.ctx.app.get(token)) - : token?.prototype; - - const methods = new Set( - this.metadataScanner.getAllFilteredMethodNames(proto), - ); - - this.ctx.writeToStdout('\n'); - this.ctx.writeToStdout(`${clc.green('Methods')}: \n`); - methods.forEach(methodName => - this.ctx.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), - ); - this.ctx.writeToStdout('\n'); - } -} diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts new file mode 100644 index 00000000000..1b1b713cfbf --- /dev/null +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -0,0 +1,57 @@ +import type { Type } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplFunction } from '../repl-function'; +import type { ModuleDebugEntry } from '../repl-context'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class DebugReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'debug', + description: '', + signature: '(moduleCls?: ClassRef | string) => void', + }; + + action(moduleCls?: Type | string): void { + this.ctx.writeToStdout('\n'); + + if (moduleCls) { + const token = + typeof moduleCls === 'function' ? moduleCls.name : moduleCls; + const moduleEntry = this.ctx.debugRegistry[token]; + if (!moduleEntry) { + return this.logger.error( + `"${token}" has not been found in the modules registry`, + ); + } + this.printCtrlsAndProviders(token, moduleEntry); + } else { + Object.keys(this.ctx.debugRegistry).forEach(moduleKey => { + this.printCtrlsAndProviders( + moduleKey, + this.ctx.debugRegistry[moduleKey], + ); + }); + } + this.ctx.writeToStdout('\n'); + } + + private printCtrlsAndProviders( + moduleName: string, + moduleDebugEntry: ModuleDebugEntry, + ) { + const printCollection = (collection: keyof ModuleDebugEntry) => { + const collectionEntries = Object.keys(moduleDebugEntry[collection]); + if (collectionEntries.length <= 0) { + return; + } + this.ctx.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); + collectionEntries.forEach(provider => + this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), + ); + }; + + this.ctx.writeToStdout(`${clc.green(moduleName)}: \n`); + printCollection('controllers'); + printCollection('providers'); + } +} diff --git a/packages/core/repl/native-functions/get-relp-fn.ts b/packages/core/repl/native-functions/get-relp-fn.ts new file mode 100644 index 00000000000..870a7297d46 --- /dev/null +++ b/packages/core/repl/native-functions/get-relp-fn.ts @@ -0,0 +1,17 @@ +import type { Type } from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class GetReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'get', + signature: '(token: InjectionToken) => any', + description: + 'Retrieves an instance of either injectable or controller, otherwise, throws exception.', + aliases: ['$'], + }; + + action(token: string | symbol | Function | Type): any { + this.ctx.app.get(token); + } +} diff --git a/packages/core/repl/native-functions/help-repl-fn.ts b/packages/core/repl/native-functions/help-repl-fn.ts new file mode 100644 index 00000000000..b1961d2034d --- /dev/null +++ b/packages/core/repl/native-functions/help-repl-fn.ts @@ -0,0 +1,30 @@ +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class HelpReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'help', + signature: '() => void', + description: 'Display all available REPL native functions.', + }; + + action(): void { + const buildHelpMessage = ({ name, description }: ReplFnDefinition) => + clc.cyanBright(name) + + (description ? ` ${clc.bold('-')} ${description}` : ''); + + const sortedNativeFunctions = this.ctx.nativeFunctions + .map(nativeFunction => nativeFunction.fnDefinition) + .sort((a, b) => (a.name < b.name ? -1 : 1)); + + this.ctx.writeToStdout( + `You can call ${clc.bold( + '.help', + )} on any function listed below (e.g.: ${clc.bold('help.help')}):\n\n` + + sortedNativeFunctions.map(buildHelpMessage).join('\n') + + // Without the following LF the last item won't be displayed + '\n', + ); + } +} diff --git a/packages/core/repl/native-functions/index.ts b/packages/core/repl/native-functions/index.ts new file mode 100644 index 00000000000..8799e685d0c --- /dev/null +++ b/packages/core/repl/native-functions/index.ts @@ -0,0 +1,6 @@ +export * from './help-repl-fn'; +export * from './get-relp-fn'; +export * from './resolve-repl-fn'; +export * from './select-relp-fn'; +export * from './debug-repl-fn'; +export * from './methods-repl-fn'; diff --git a/packages/core/repl/native-functions/methods-repl-fn.ts b/packages/core/repl/native-functions/methods-repl-fn.ts new file mode 100644 index 00000000000..3342eca82f3 --- /dev/null +++ b/packages/core/repl/native-functions/methods-repl-fn.ts @@ -0,0 +1,33 @@ +import type { Type } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { MetadataScanner } from '../../metadata-scanner'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class MethodsReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'methods', + description: 'Display all public methods available on a given provider.', + signature: '(token: ClassRef | string) => void', + }; + + private readonly metadataScanner = new MetadataScanner(); + + action(token: Type | string): void { + const proto = + typeof token !== 'function' + ? Object.getPrototypeOf(this.ctx.app.get(token)) + : token?.prototype; + + const methods = new Set( + this.metadataScanner.getAllFilteredMethodNames(proto), + ); + + this.ctx.writeToStdout('\n'); + this.ctx.writeToStdout(`${clc.green('Methods')}: \n`); + methods.forEach(methodName => + this.ctx.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), + ); + this.ctx.writeToStdout('\n'); + } +} diff --git a/packages/core/repl/native-functions/resolve-repl-fn.ts b/packages/core/repl/native-functions/resolve-repl-fn.ts new file mode 100644 index 00000000000..7b278d8b770 --- /dev/null +++ b/packages/core/repl/native-functions/resolve-repl-fn.ts @@ -0,0 +1,19 @@ +import type { Type } from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class ResolveReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'resolve', + description: + 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception', + signature: '(token: InjectionToken, contextId: any) => Promise', + }; + + action( + token: string | symbol | Function | Type, + contextId: any, + ): Promise { + return this.ctx.app.resolve(token, contextId); + } +} diff --git a/packages/core/repl/native-functions/select-relp-fn.ts b/packages/core/repl/native-functions/select-relp-fn.ts new file mode 100644 index 00000000000..f35346cd100 --- /dev/null +++ b/packages/core/repl/native-functions/select-relp-fn.ts @@ -0,0 +1,20 @@ +import type { + DynamicModule, + INestApplicationContext, + Type, +} from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class SelectReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'select', + description: + 'Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.', + signature: '(token: DynamicModule | ClassRef) => INestApplicationContext', + }; + + action(token: DynamicModule | Type): INestApplicationContext { + return this.ctx.app.select(token); + } +} From 964d02df6fe0dae7dde7640ffa79c93b26e64d81 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 06:40:54 -0400 Subject: [PATCH 08/27] refactor(core): rename `ReplContext#initialize` to `initializeContext` --- packages/core/repl/repl-context.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index 8be2e600a3e..2a5c56291b5 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -31,11 +31,11 @@ export class ReplContext { nativeFunctionsClassRefs?: ReplFunctionClass[], ) { this.container = (app as any).container; - this.initialize(); + this.initializeContext(); this.initializeNativeFunctions(nativeFunctionsClassRefs || []); } - private initialize() { + private initializeContext() { const globalRef = globalThis; const modules = this.container.getModules(); From d38a4e6a4ca2bba1e9c8097bd62ce62de1158190 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 06:42:40 -0400 Subject: [PATCH 09/27] refactor(core): extract `loadNativeFunctionsIntoContext` from `repl.ts` --- .../load-native-functions-into-context.ts | 49 +++++++++++++++++++ packages/core/repl/repl.ts | 48 +----------------- 2 files changed, 50 insertions(+), 47 deletions(-) create mode 100644 packages/core/repl/load-native-functions-into-context.ts diff --git a/packages/core/repl/load-native-functions-into-context.ts b/packages/core/repl/load-native-functions-into-context.ts new file mode 100644 index 00000000000..af367d369d0 --- /dev/null +++ b/packages/core/repl/load-native-functions-into-context.ts @@ -0,0 +1,49 @@ +import * as _repl from 'repl'; +import { ReplContext } from './repl-context'; +import { ReplFunction } from './repl-function'; +import { ReplFunctionClass } from './repl.interfaces'; + +export function loadNativeFunctionsIntoContext( + replServerContext: _repl.REPLServer['context'], + replContext: ReplContext, +) { + const registerFunction = ( + nativeFunction: InstanceType, + ): void => { + // Bind the method to REPL's context: + replServerContext[nativeFunction.fnDefinition.name] = + nativeFunction.action.bind(nativeFunction); + + // Load the help trigger as a `help` getter on each native function: + const functionBoundRef: ReplFunction['action'] = + replServerContext[nativeFunction.fnDefinition.name]; + Object.defineProperty(functionBoundRef, 'help', { + enumerable: false, + configurable: false, + get: () => + // Dynamically builds the help message as will unlikely to be called + // several times. + replContext.writeToStdout(nativeFunction.makeHelpMessage()), + }); + }; + + const aliasesNativeFunctions = replContext.nativeFunctions + .filter(nativeFunction => nativeFunction.fnDefinition.aliases) + .flatMap(nativeFunction => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + nativeFunction.fnDefinition.aliases!.map(aliasFnName => { + const aliasNativeFunction: InstanceType = + Object.create(nativeFunction); + aliasNativeFunction.fnDefinition = { + name: aliasFnName, + description: aliasNativeFunction.fnDefinition.description, + signature: aliasNativeFunction.fnDefinition.signature, + }; + + return aliasNativeFunction; + }), + ); + + replContext.nativeFunctions.push(...aliasesNativeFunctions); + replContext.nativeFunctions.forEach(registerFunction); +} diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts index a41a25af5a9..ff497c0d0d8 100644 --- a/packages/core/repl/repl.ts +++ b/packages/core/repl/repl.ts @@ -2,55 +2,9 @@ import { Logger, Type } from '@nestjs/common'; import * as _repl from 'repl'; import { NestFactory } from '../nest-factory'; import { REPL_INITIALIZED_MESSAGE } from './constants'; +import { loadNativeFunctionsIntoContext } from './load-native-functions-into-context'; import { ReplContext } from './repl-context'; -import { ReplFunction } from './repl-function'; import { ReplLogger } from './repl-logger'; -import { ReplFunctionClass } from './repl.interfaces'; - -function loadNativeFunctionsIntoContext( - replServerContext: _repl.REPLServer['context'], - replContext: ReplContext, -) { - const registerFunction = ( - nativeFunction: InstanceType, - ): void => { - // Bind the method to REPL's context: - replServerContext[nativeFunction.fnDefinition.name] = - nativeFunction.action.bind(nativeFunction); - - // Load the help trigger as a `help` getter on each native function: - const functionBoundRef: ReplFunction['action'] = - replServerContext[nativeFunction.fnDefinition.name]; - Object.defineProperty(functionBoundRef, 'help', { - enumerable: false, - configurable: false, - get: () => - // Dynamically builds the help message as will unlikely to be called - // several times. - replContext.writeToStdout(nativeFunction.makeHelpMessage()), - }); - }; - - const aliasesNativeFunctions = replContext.nativeFunctions - .filter(nativeFunction => nativeFunction.fnDefinition.aliases) - .flatMap(nativeFunction => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - nativeFunction.fnDefinition.aliases!.map(aliasFnName => { - const aliasNativeFunction: InstanceType = - Object.create(nativeFunction); - aliasNativeFunction.fnDefinition = { - name: aliasFnName, - description: aliasNativeFunction.fnDefinition.description, - signature: aliasNativeFunction.fnDefinition.signature, - }; - - return aliasNativeFunction; - }), - ); - - replContext.nativeFunctions.push(...aliasesNativeFunctions); - replContext.nativeFunctions.forEach(registerFunction); -} export async function repl(module: Type) { const app = await NestFactory.create(module, { From fbc0ab81b228ae8b78625f1467a9c0667a5ba08b Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 06:49:09 -0400 Subject: [PATCH 10/27] refactor(core): clean-up `ReplContext` fields --- packages/core/repl/repl-context.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index 2a5c56291b5..6b350dd4f2d 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -20,10 +20,10 @@ export type ModuleDebugEntry = { }; export class ReplContext { - public nativeFunctions: InstanceType[]; + private readonly container: NestContainer; + public nativeFunctions: InstanceType[]; public debugRegistry: Record = {}; - public readonly container: NestContainer; public readonly logger = new Logger(ReplContext.name); constructor( @@ -35,6 +35,10 @@ export class ReplContext { this.initializeNativeFunctions(nativeFunctionsClassRefs || []); } + public writeToStdout(text: string) { + process.stdout.write(text); + } + private initializeContext() { const globalRef = globalThis; const modules = this.container.getModules(); @@ -108,8 +112,4 @@ export class ReplContext { .concat(nativeFunctionsClassRefs) .map(NativeFn => new NativeFn(this)); } - - public writeToStdout(text: string) { - process.stdout.write(text); - } } From 092f350eb8acdc7da6f828f27b2f81189a39b227 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 07:51:26 -0400 Subject: [PATCH 11/27] refactor(core): replace array by map for `nativeFunctions` field --- .../load-native-functions-into-context.ts | 24 +++---------- .../repl/native-functions/help-repl-fn.ts | 16 +++++---- packages/core/repl/repl-context.ts | 35 +++++++++++++++---- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/core/repl/load-native-functions-into-context.ts b/packages/core/repl/load-native-functions-into-context.ts index af367d369d0..6dd0a5c4e02 100644 --- a/packages/core/repl/load-native-functions-into-context.ts +++ b/packages/core/repl/load-native-functions-into-context.ts @@ -7,7 +7,7 @@ export function loadNativeFunctionsIntoContext( replServerContext: _repl.REPLServer['context'], replContext: ReplContext, ) { - const registerFunction = ( + const registerFunctionToReplServerContext = ( nativeFunction: InstanceType, ): void => { // Bind the method to REPL's context: @@ -27,23 +27,7 @@ export function loadNativeFunctionsIntoContext( }); }; - const aliasesNativeFunctions = replContext.nativeFunctions - .filter(nativeFunction => nativeFunction.fnDefinition.aliases) - .flatMap(nativeFunction => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - nativeFunction.fnDefinition.aliases!.map(aliasFnName => { - const aliasNativeFunction: InstanceType = - Object.create(nativeFunction); - aliasNativeFunction.fnDefinition = { - name: aliasFnName, - description: aliasNativeFunction.fnDefinition.description, - signature: aliasNativeFunction.fnDefinition.signature, - }; - - return aliasNativeFunction; - }), - ); - - replContext.nativeFunctions.push(...aliasesNativeFunctions); - replContext.nativeFunctions.forEach(registerFunction); + for (const [, nativeFunction] of replContext.nativeFunctions) { + registerFunctionToReplServerContext(nativeFunction); + } } diff --git a/packages/core/repl/native-functions/help-repl-fn.ts b/packages/core/repl/native-functions/help-repl-fn.ts index b1961d2034d..913ced7ee91 100644 --- a/packages/core/repl/native-functions/help-repl-fn.ts +++ b/packages/core/repl/native-functions/help-repl-fn.ts @@ -1,3 +1,4 @@ +import { iterate } from 'iterare'; import { clc } from '@nestjs/common/utils/cli-colors.util'; import { ReplFunction } from '../repl-function'; import type { ReplFnDefinition } from '../repl.interfaces'; @@ -9,20 +10,21 @@ export class HelpReplFn extends ReplFunction { description: 'Display all available REPL native functions.', }; - action(): void { - const buildHelpMessage = ({ name, description }: ReplFnDefinition) => - clc.cyanBright(name) + - (description ? ` ${clc.bold('-')} ${description}` : ''); + static buildHelpMessage = ({ name, description }: ReplFnDefinition) => + clc.cyanBright(name) + + (description ? ` ${clc.bold('-')} ${description}` : ''); - const sortedNativeFunctions = this.ctx.nativeFunctions - .map(nativeFunction => nativeFunction.fnDefinition) + action(): void { + const sortedNativeFunctions = iterate(this.ctx.nativeFunctions) + .map(([, nativeFunction]) => nativeFunction.fnDefinition) + .toArray() .sort((a, b) => (a.name < b.name ? -1 : 1)); this.ctx.writeToStdout( `You can call ${clc.bold( '.help', )} on any function listed below (e.g.: ${clc.bold('help.help')}):\n\n` + - sortedNativeFunctions.map(buildHelpMessage).join('\n') + + sortedNativeFunctions.map(HelpReplFn.buildHelpMessage).join('\n') + // Without the following LF the last item won't be displayed '\n', ); diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index 6b350dd4f2d..cf0e81ce2dc 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -20,17 +20,19 @@ export type ModuleDebugEntry = { }; export class ReplContext { - private readonly container: NestContainer; - - public nativeFunctions: InstanceType[]; - public debugRegistry: Record = {}; public readonly logger = new Logger(ReplContext.name); + public debugRegistry: Record = {}; + public readonly nativeFunctions = new Map< + string, + InstanceType + >(); + private readonly container: NestContainer; constructor( public readonly app: INestApplication, nativeFunctionsClassRefs?: ReplFunctionClass[], ) { - this.container = (app as any).container; + this.container = (app as any).container; // Using `any` because `app.container` is not public. this.initializeContext(); this.initializeNativeFunctions(nativeFunctionsClassRefs || []); } @@ -39,6 +41,23 @@ export class ReplContext { process.stdout.write(text); } + public addNativeFunction(NativeFunction: ReplFunctionClass): void { + const nativeFunction = new NativeFunction(this); + + this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction); + + nativeFunction.fnDefinition.aliases?.forEach(aliaseName => { + const aliasNativeFunction: InstanceType = + Object.create(nativeFunction); + aliasNativeFunction.fnDefinition = { + name: aliaseName, + description: aliasNativeFunction.fnDefinition.description, + signature: aliasNativeFunction.fnDefinition.signature, + }; + this.nativeFunctions.set(aliaseName, aliasNativeFunction); + }); + } + private initializeContext() { const globalRef = globalThis; const modules = this.container.getModules(); @@ -108,8 +127,10 @@ export class ReplContext { MethodsReplFn, ]; - this.nativeFunctions = builtInFunctionsClassRefs + builtInFunctionsClassRefs .concat(nativeFunctionsClassRefs) - .map(NativeFn => new NativeFn(this)); + .forEach(NativeFunction => { + this.addNativeFunction(NativeFunction); + }); } } From 32c0a744fdbb0ccb6706660aba454cf789de41a0 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 08:19:24 -0400 Subject: [PATCH 12/27] feat(core): add description to `debug` native repl function --- packages/core/repl/native-functions/debug-repl-fn.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts index 1b1b713cfbf..0756296d2ea 100644 --- a/packages/core/repl/native-functions/debug-repl-fn.ts +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -7,7 +7,8 @@ import type { ReplFnDefinition } from '../repl.interfaces'; export class DebugReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'debug', - description: '', + description: + 'Allows you to process the identification of the problem in stages, isolating the source of the problem and then correcting the problem or determining a way to solve it.', signature: '(moduleCls?: ClassRef | string) => void', }; From 59965cf6312706e73b02f04d863716fced71f593 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 08:41:51 -0400 Subject: [PATCH 13/27] test(core,integration): fix repl test suite for the new api --- integration/repl/e2e/repl.spec.ts | 10 +- .../repl/native-functions/debug-repl-fn.ts | 32 ++-- .../repl/native-functions/methods-repl-fn.ts | 2 +- .../native-functions/debug-relp-fn.spec.ts | 122 ++++++++++++++ .../repl/native-functions/get-relp-fn.spec.ts | 47 ++++++ .../native-functions/methods-relp-fn.spec.ts | 108 ++++++++++++ .../native-functions/resolve-relp-fn.spec.ts | 49 ++++++ .../native-functions/select-relp-fn.spec.ts | 47 ++++++ packages/core/test/repl/repl-context.spec.ts | 158 ------------------ 9 files changed, 397 insertions(+), 178 deletions(-) create mode 100644 packages/core/test/repl/native-functions/debug-relp-fn.spec.ts create mode 100644 packages/core/test/repl/native-functions/get-relp-fn.spec.ts create mode 100644 packages/core/test/repl/native-functions/methods-relp-fn.spec.ts create mode 100644 packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts create mode 100644 packages/core/test/repl/native-functions/select-relp-fn.spec.ts diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts index 318b16f7a4b..1358a267a13 100644 --- a/integration/repl/e2e/repl.spec.ts +++ b/integration/repl/e2e/repl.spec.ts @@ -59,10 +59,10 @@ ${prompt}`); expect(outputText).to.equal( ` -UsersModule: - - controllers: +UsersModule: + - controllers: ◻ UsersController - - providers: + - providers: ◻ UsersService ◻ UsersRepository @@ -82,7 +82,7 @@ ${prompt}`, expect(outputText).to.equal( ` -Methods: +Methods: ◻ find ${prompt}`, @@ -93,7 +93,7 @@ ${prompt}`, expect(outputText).to.equal( ` -Methods: +Methods: ◻ create ◻ findAll ◻ findOne diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts index 0756296d2ea..b979ec3303b 100644 --- a/packages/core/repl/native-functions/debug-repl-fn.ts +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -1,4 +1,4 @@ -import type { Type } from '@nestjs/common'; +import type { Type, InjectionToken } from '@nestjs/common'; import { clc } from '@nestjs/common/utils/cli-colors.util'; import { ReplFunction } from '../repl-function'; import type { ModuleDebugEntry } from '../repl-context'; @@ -40,19 +40,23 @@ export class DebugReplFn extends ReplFunction { moduleName: string, moduleDebugEntry: ModuleDebugEntry, ) { - const printCollection = (collection: keyof ModuleDebugEntry) => { - const collectionEntries = Object.keys(moduleDebugEntry[collection]); - if (collectionEntries.length <= 0) { - return; - } - this.ctx.writeToStdout(` ${clc.yellow(`- ${collection}`)}: \n`); - collectionEntries.forEach(provider => - this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), - ); - }; + this.ctx.writeToStdout(`${clc.green(moduleName)}:\n`); + this.printCollection('controllers', moduleDebugEntry['controllers']); + this.printCollection('providers', moduleDebugEntry['providers']); + } + + private printCollection( + title: string, + collectionValue: Record, + ) { + const collectionEntries = Object.keys(collectionValue); + if (collectionEntries.length <= 0) { + return; + } - this.ctx.writeToStdout(`${clc.green(moduleName)}: \n`); - printCollection('controllers'); - printCollection('providers'); + this.ctx.writeToStdout(` ${clc.yellow(`- ${title}`)}:\n`); + collectionEntries.forEach(provider => + this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), + ); } } diff --git a/packages/core/repl/native-functions/methods-repl-fn.ts b/packages/core/repl/native-functions/methods-repl-fn.ts index 3342eca82f3..c14f2603813 100644 --- a/packages/core/repl/native-functions/methods-repl-fn.ts +++ b/packages/core/repl/native-functions/methods-repl-fn.ts @@ -24,7 +24,7 @@ export class MethodsReplFn extends ReplFunction { ); this.ctx.writeToStdout('\n'); - this.ctx.writeToStdout(`${clc.green('Methods')}: \n`); + this.ctx.writeToStdout(`${clc.green('Methods')}:\n`); methods.forEach(methodName => this.ctx.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), ); diff --git a/packages/core/test/repl/native-functions/debug-relp-fn.spec.ts b/packages/core/test/repl/native-functions/debug-relp-fn.spec.ts new file mode 100644 index 00000000000..e7ddc4a04bc --- /dev/null +++ b/packages/core/test/repl/native-functions/debug-relp-fn.spec.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { DebugReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('DebugReplFn', () => { + let debugReplFn: DebugReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + const aModuleRef = await container.addModule(class ModuleA {}, []); + const bModuleRef = await container.addModule(class ModuleB {}, []); + + container.addController(class ControllerA {}, aModuleRef.token); + container.addProvider(class ProviderA1 {}, aModuleRef.token); + container.addProvider(class ProviderA2 {}, aModuleRef.token); + + container.addProvider(class ProviderB1 {}, bModuleRef.token); + container.addProvider(class ProviderB2 {}, bModuleRef.token); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + debugReplFn = replContext.nativeFunctions.get('debug') as DebugReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "debug"', () => { + expect(debugReplFn).to.not.be.undefined; + expect(debugReplFn.fnDefinition.name).to.eql('debug'); + }); + + describe('action', () => { + it('should print all modules along with their controllers and providers', () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action(); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 +ModuleB: + - providers: + ◻ ProviderB1 + ◻ ProviderB2 + +`); + }); + + describe('when module passed as a class reference', () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action(class ModuleA {}); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + describe("when module passed as a string (module's key)", () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action('ModuleA'); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/get-relp-fn.spec.ts b/packages/core/test/repl/native-functions/get-relp-fn.spec.ts new file mode 100644 index 00000000000..aec8f335fac --- /dev/null +++ b/packages/core/test/repl/native-functions/get-relp-fn.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { GetReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('GetReplFn', () => { + let getReplFn: GetReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + getReplFn = replContext.nativeFunctions.get('get') as GetReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "get"', () => { + expect(getReplFn).to.not.be.undefined; + expect(getReplFn.fnDefinition.name).to.eql('get'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', () => { + const token = 'test'; + getReplFn.action(token); + expect(mockApp.get.calledWith(token)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/methods-relp-fn.spec.ts b/packages/core/test/repl/native-functions/methods-relp-fn.spec.ts new file mode 100644 index 00000000000..a45f5c2551f --- /dev/null +++ b/packages/core/test/repl/native-functions/methods-relp-fn.spec.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { MethodsReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('MethodsReplFn', () => { + let methodsReplFn: MethodsReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + const aModuleRef = await container.addModule(class ModuleA {}, []); + const bModuleRef = await container.addModule(class ModuleB {}, []); + + container.addController(class ControllerA {}, aModuleRef.token); + container.addProvider(class ProviderA1 {}, aModuleRef.token); + container.addProvider(class ProviderA2 {}, aModuleRef.token); + + container.addProvider(class ProviderB1 {}, bModuleRef.token); + container.addProvider(class ProviderB2 {}, bModuleRef.token); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + methodsReplFn = replContext.nativeFunctions.get('methods') as MethodsReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "methods"', () => { + expect(methodsReplFn).to.not.be.undefined; + expect(methodsReplFn.fnDefinition.name).to.eql('methods'); + }); + + describe('action', () => { + describe('when token is a class reference', () => { + it('should print all class methods', () => { + class BaseService { + create() {} + } + class TestService extends BaseService { + findAll() {} + findOne() {} + } + + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + methodsReplFn.action(TestService); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + ◻ create + +`); + }); + }); + + describe('when token is a string', () => { + it('should grab provider from the container and print its all methods', () => { + class ProviderA1 { + findAll() {} + findOne() {} + } + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + mockApp.get.callsFake(() => new ProviderA1()); + + methodsReplFn.action('ProviderA1'); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + +`); + }); + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts b/packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts new file mode 100644 index 00000000000..cf78bbd40e3 --- /dev/null +++ b/packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ResolveReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('ResolveReplFn', () => { + let resolveReplFn: ResolveReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + resolveReplFn = replContext.nativeFunctions.get('resolve') as ResolveReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "resolve"', () => { + expect(resolveReplFn).to.not.be.undefined; + expect(resolveReplFn.fnDefinition.name).to.eql('resolve'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', async () => { + const token = 'test'; + const contextId = {}; + + await resolveReplFn.action(token, contextId); + expect(mockApp.resolve.calledWith(token, contextId)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/select-relp-fn.spec.ts b/packages/core/test/repl/native-functions/select-relp-fn.spec.ts new file mode 100644 index 00000000000..22a033e542c --- /dev/null +++ b/packages/core/test/repl/native-functions/select-relp-fn.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { SelectReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('SelectReplFn', () => { + let selectReplFn: SelectReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + selectReplFn = replContext.nativeFunctions.get('select') as SelectReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "select"', () => { + expect(selectReplFn).to.not.be.undefined; + expect(selectReplFn.fnDefinition.name).to.eql('select'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', () => { + const moduleCls = class TestModule {}; + selectReplFn.action(moduleCls); + expect(mockApp.select.calledWith(moduleCls)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/repl-context.spec.ts b/packages/core/test/repl/repl-context.spec.ts index 4446dda811a..771af69d4e5 100644 --- a/packages/core/test/repl/repl-context.spec.ts +++ b/packages/core/test/repl/repl-context.spec.ts @@ -1,5 +1,3 @@ -import { clc } from '@nestjs/common/utils/cli-colors.util'; -import { expect } from 'chai'; import * as sinon from 'sinon'; import { NestContainer } from '../../injector/container'; import { ReplContext } from '../../repl/repl-context'; @@ -15,15 +13,6 @@ describe('ReplContext', () => { before(async () => { const container = new NestContainer(); - const aModuleRef = await container.addModule(class ModuleA {}, []); - const bModuleRef = await container.addModule(class ModuleB {}, []); - - container.addController(class ControllerA {}, aModuleRef.token); - container.addProvider(class ProviderA1 {}, aModuleRef.token); - container.addProvider(class ProviderA2 {}, aModuleRef.token); - - container.addProvider(class ProviderB1 {}, bModuleRef.token); - container.addProvider(class ProviderB2 {}, bModuleRef.token); mockApp = { container, @@ -34,152 +23,5 @@ describe('ReplContext', () => { replContext = new ReplContext(mockApp as any); }); - beforeEach(() => { - sinon.stub(clc, 'yellow').callsFake(text => text); - sinon.stub(clc, 'green').callsFake(text => text); - }); afterEach(() => sinon.restore()); - - describe('debug', () => { - it('should print all modules along with their controllers and providers', () => { - let outputText = ''; - - sinon - .stub(replContext as any, 'writeToStdout') - .callsFake(text => (outputText += text)); - replContext.debug(); - - expect(outputText).to.equal(` -ModuleA: - - controllers: - ◻ ControllerA - - providers: - ◻ ProviderA1 - ◻ ProviderA2 -ModuleB: - - providers: - ◻ ProviderB1 - ◻ ProviderB2 - -`); - }); - - describe('when module passed as a class reference', () => { - it("should print a specified module's controllers and providers", () => { - let outputText = ''; - - sinon - .stub(replContext as any, 'writeToStdout') - .callsFake(text => (outputText += text)); - replContext.debug(class ModuleA {}); - - expect(outputText).to.equal(` -ModuleA: - - controllers: - ◻ ControllerA - - providers: - ◻ ProviderA1 - ◻ ProviderA2 - -`); - }); - }); - describe("when module passed as a string (module's key)", () => { - it("should print a specified module's controllers and providers", () => { - let outputText = ''; - - sinon - .stub(replContext as any, 'writeToStdout') - .callsFake(text => (outputText += text)); - replContext.debug('ModuleA'); - - expect(outputText).to.equal(` -ModuleA: - - controllers: - ◻ ControllerA - - providers: - ◻ ProviderA1 - ◻ ProviderA2 - -`); - }); - }); - }); - - describe('methods', () => { - describe('when token is a class reference', () => { - it('should print all class methods', () => { - class BaseService { - create() {} - } - class TestService extends BaseService { - findAll() {} - findOne() {} - } - - let outputText = ''; - - sinon - .stub(replContext as any, 'writeToStdout') - .callsFake(text => (outputText += text)); - replContext.methods(TestService); - - expect(outputText).to.equal(` -Methods: - ◻ findAll - ◻ findOne - ◻ create - -`); - }); - }); - - describe('when token is a string', () => { - it('should grab provider from the container and print its all methods', () => { - class ProviderA1 { - findAll() {} - findOne() {} - } - let outputText = ''; - - sinon - .stub(replContext as any, 'writeToStdout') - .callsFake(text => (outputText += text)); - - mockApp.get.callsFake(() => new ProviderA1()); - replContext.methods('ProviderA1'); - - expect(outputText).to.equal(` -Methods: - ◻ findAll - ◻ findOne - -`); - }); - }); - }); - - describe('get', () => { - it('should pass arguments down to the application context', () => { - const token = 'test'; - replContext.get(token); - expect(mockApp.get.calledWith(token)).to.be.true; - }); - }); - describe('resolve', () => { - it('should pass arguments down to the application context', async () => { - const token = 'test'; - const contextId = {}; - - await replContext.resolve(token, contextId); - expect(mockApp.resolve.calledWith(token, contextId)).to.be.true; - }); - }); - describe('select', () => { - it('should pass arguments down to the application context', () => { - const moduleCls = class TestModule {}; - replContext.select(moduleCls); - expect(mockApp.select.calledWith(moduleCls)).to.be.true; - }); - }); }); From 5ab8800bcb648880035db52b8d18923ffd3f297c Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 11:05:12 -0400 Subject: [PATCH 14/27] fix(core): add missing return to `GetReplFn#action` --- packages/core/repl/native-functions/get-relp-fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/get-relp-fn.ts b/packages/core/repl/native-functions/get-relp-fn.ts index 870a7297d46..6de27691896 100644 --- a/packages/core/repl/native-functions/get-relp-fn.ts +++ b/packages/core/repl/native-functions/get-relp-fn.ts @@ -12,6 +12,6 @@ export class GetReplFn extends ReplFunction { }; action(token: string | symbol | Function | Type): any { - this.ctx.app.get(token); + return this.ctx.app.get(token); } } From 833d16e21cc6447166aaf5a04e5cc51b23a65ac1 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 31 May 2022 20:35:33 -0400 Subject: [PATCH 15/27] test(core): add few missing tests for repl scope --- integration/repl/e2e/repl.spec.ts | 132 +++++++++++++++++- .../native-functions/help-relp-fn.spec.ts | 67 +++++++++ packages/core/test/repl/repl-context.spec.ts | 11 ++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/repl/native-functions/help-relp-fn.spec.ts diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts index 1358a267a13..2062dc05860 100644 --- a/integration/repl/e2e/repl.spec.ts +++ b/integration/repl/e2e/repl.spec.ts @@ -1,5 +1,14 @@ import { clc } from '@nestjs/common/utils/cli-colors.util'; import { repl } from '@nestjs/core'; +import { ReplContext } from '@nestjs/core/repl/repl-context'; +import { + HelpReplFn, + GetReplFn, + ResolveReplFn, + SelectReplFn, + DebugReplFn, + MethodsReplFn, +} from '@nestjs/core/repl/native-functions'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { AppModule } from '../src/app.module'; @@ -9,8 +18,13 @@ const prompt = '\u001b[1G\u001b[0J\u001b[32m>\u001b[0m \u001b[3G'; describe('REPL', () => { beforeEach(() => { - sinon.stub(clc, 'yellow').callsFake(text => text); + // To avoid coloring the output: + sinon.stub(clc, 'bold').callsFake(text => text); sinon.stub(clc, 'green').callsFake(text => text); + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'red').callsFake(text => text); + sinon.stub(clc, 'magentaBright').callsFake(text => text); + sinon.stub(clc, 'cyanBright').callsFake(text => text); }); afterEach(() => { sinon.restore(); @@ -103,4 +117,120 @@ Methods: ${prompt}`, ); }); + + describe('.help', () => { + it(`Typing "help.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new HelpReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'help.help'); + + expect(outputText).to.equal(`${description} +Interface: help${signature} +${prompt}`); + }); + + it(`Typing "get.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new GetReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'get.help'); + + expect(outputText).to.equal(`${description} +Interface: get${signature} +${prompt}`); + }); + + it(`Typing "resolve.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new ResolveReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'resolve.help'); + + expect(outputText).to.equal(`${description} +Interface: resolve${signature} +${prompt}`); + }); + + it(`Typing "select.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new SelectReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'select.help'); + + expect(outputText).to.equal(`${description} +Interface: select${signature} +${prompt}`); + }); + + it(`Typing "debug.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new DebugReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'debug.help'); + + expect(outputText).to.equal(`${description} +Interface: debug${signature} +${prompt}`); + }); + + it(`Typing "methods.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new MethodsReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'methods.help'); + + expect(outputText).to.equal(`${description} +Interface: methods${signature} +${prompt}`); + }); + }); }); diff --git a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts new file mode 100644 index 00000000000..ccf5d22071c --- /dev/null +++ b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { HelpReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('HelpReplFn', () => { + let helpReplFn: HelpReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + helpReplFn = replContext.nativeFunctions.get('help') as HelpReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'bold').callsFake(text => text); + sinon.stub(clc, 'cyanBright').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "help"', () => { + expect(helpReplFn).to.not.be.undefined; + expect(helpReplFn.fnDefinition.name).to.eql('help'); + }); + + describe('action', () => { + it('should print all available native functions and their description', () => { + let outputText = ''; + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + helpReplFn.action(); + + expect(outputText).to + .equal(`You can call .help on any function listed below (e.g.: help.help): + +$ - Retrieves an instance of either injectable or controller, otherwise, throws exception. +debug - Allows you to process the identification of the problem in stages, isolating the source of the problem and then correcting the problem or determining a way to solve it. +get - Retrieves an instance of either injectable or controller, otherwise, throws exception. +help - Display all available REPL native functions. +methods - Display all public methods available on a given provider. +resolve - Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception +select - Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module. +`); + }); + }); +}); diff --git a/packages/core/test/repl/repl-context.spec.ts b/packages/core/test/repl/repl-context.spec.ts index 771af69d4e5..9654dfce5e8 100644 --- a/packages/core/test/repl/repl-context.spec.ts +++ b/packages/core/test/repl/repl-context.spec.ts @@ -1,3 +1,4 @@ +import { expect } from 'chai'; import * as sinon from 'sinon'; import { NestContainer } from '../../injector/container'; import { ReplContext } from '../../repl/repl-context'; @@ -24,4 +25,14 @@ describe('ReplContext', () => { }); afterEach(() => sinon.restore()); + + it('writeToStdout', () => { + const stdOutWrite = sinon.stub(process.stdout, 'write'); + const text = sinon.stub() as unknown as string; + + replContext.writeToStdout(text); + + expect(stdOutWrite.calledOnce).to.be.true; + expect(stdOutWrite.calledWith(text)).to.be.true; + }); }); From 53fc03a82135cdaf374aee53f831e35baacfcd33 Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 11:52:47 +0200 Subject: [PATCH 16/27] Update packages/core/repl/native-functions/debug-repl-fn.ts --- packages/core/repl/native-functions/debug-repl-fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts index b979ec3303b..60f27cf6acc 100644 --- a/packages/core/repl/native-functions/debug-repl-fn.ts +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -8,7 +8,7 @@ export class DebugReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'debug', description: - 'Allows you to process the identification of the problem in stages, isolating the source of the problem and then correcting the problem or determining a way to solve it.', + 'Prints all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module.', signature: '(moduleCls?: ClassRef | string) => void', }; From 89b39f946a843d663f46c53df59a7f3500b474c0 Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 11:53:45 +0200 Subject: [PATCH 17/27] Update packages/core/repl/native-functions/debug-repl-fn.ts --- packages/core/repl/native-functions/debug-repl-fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts index 60f27cf6acc..910941584e1 100644 --- a/packages/core/repl/native-functions/debug-repl-fn.ts +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -8,7 +8,7 @@ export class DebugReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'debug', description: - 'Prints all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module.', + 'Print all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module.', signature: '(moduleCls?: ClassRef | string) => void', }; From 160b521ef7d8415e6bed7783b0928a0836dee8ef Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 11:54:07 +0200 Subject: [PATCH 18/27] Update packages/core/repl/native-functions/methods-repl-fn.ts --- packages/core/repl/native-functions/methods-repl-fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/methods-repl-fn.ts b/packages/core/repl/native-functions/methods-repl-fn.ts index c14f2603813..b8deb83fc5a 100644 --- a/packages/core/repl/native-functions/methods-repl-fn.ts +++ b/packages/core/repl/native-functions/methods-repl-fn.ts @@ -7,7 +7,7 @@ import type { ReplFnDefinition } from '../repl.interfaces'; export class MethodsReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'methods', - description: 'Display all public methods available on a given provider.', + description: 'Display all public methods available on a given provider or controller.', signature: '(token: ClassRef | string) => void', }; From 99c6c628a4958d3f12252eba95ffcab614fcc74d Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 11:54:26 +0200 Subject: [PATCH 19/27] Update packages/core/repl/native-functions/resolve-repl-fn.ts --- packages/core/repl/native-functions/resolve-repl-fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl/native-functions/resolve-repl-fn.ts b/packages/core/repl/native-functions/resolve-repl-fn.ts index 7b278d8b770..fe587682d1d 100644 --- a/packages/core/repl/native-functions/resolve-repl-fn.ts +++ b/packages/core/repl/native-functions/resolve-repl-fn.ts @@ -6,7 +6,7 @@ export class ResolveReplFn extends ReplFunction { public fnDefinition: ReplFnDefinition = { name: 'resolve', description: - 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception', + 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.', signature: '(token: InjectionToken, contextId: any) => Promise', }; From c18f4b9f36bb1e21a79a14581fc0835cf78c85c1 Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 12:01:09 +0200 Subject: [PATCH 20/27] Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts --- packages/core/test/repl/native-functions/help-relp-fn.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts index ccf5d22071c..fcc832f870a 100644 --- a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts +++ b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts @@ -55,7 +55,7 @@ describe('HelpReplFn', () => { .equal(`You can call .help on any function listed below (e.g.: help.help): $ - Retrieves an instance of either injectable or controller, otherwise, throws exception. -debug - Allows you to process the identification of the problem in stages, isolating the source of the problem and then correcting the problem or determining a way to solve it. +debug - Print all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module. get - Retrieves an instance of either injectable or controller, otherwise, throws exception. help - Display all available REPL native functions. methods - Display all public methods available on a given provider. From b8e48632e8b6d20f3f5ae6838cf0dacdd4ea92cd Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 12:01:16 +0200 Subject: [PATCH 21/27] Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts --- packages/core/test/repl/native-functions/help-relp-fn.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts index fcc832f870a..b9ca04af3fd 100644 --- a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts +++ b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts @@ -58,7 +58,7 @@ $ - Retrieves an instance of either injectable or controller, otherwise, throws debug - Print all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module. get - Retrieves an instance of either injectable or controller, otherwise, throws exception. help - Display all available REPL native functions. -methods - Display all public methods available on a given provider. +methods - Display all public methods available on a given provider or controller. resolve - Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception select - Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module. `); From 09d6c4b8575b17a9ed1332e6f893d1e6c5347170 Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Wed, 1 Jun 2022 12:01:22 +0200 Subject: [PATCH 22/27] Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts --- packages/core/test/repl/native-functions/help-relp-fn.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts index b9ca04af3fd..441d14c43c9 100644 --- a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts +++ b/packages/core/test/repl/native-functions/help-relp-fn.spec.ts @@ -59,7 +59,7 @@ debug - Print all registered modules as a list together with their controllers a get - Retrieves an instance of either injectable or controller, otherwise, throws exception. help - Display all available REPL native functions. methods - Display all public methods available on a given provider or controller. -resolve - Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception +resolve - Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. select - Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module. `); }); From d8c8e671480be09df4621de051316792c9c36b29 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Wed, 1 Jun 2022 22:17:39 -0400 Subject: [PATCH 23/27] fix(core): prompt indicador respect `NO_COLOR` config --- integration/repl/e2e/repl.spec.ts | 26 +++++++++++++------------- packages/core/repl/repl.ts | 3 ++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts index 2062dc05860..c57cb828646 100644 --- a/integration/repl/e2e/repl.spec.ts +++ b/integration/repl/e2e/repl.spec.ts @@ -14,7 +14,7 @@ import * as sinon from 'sinon'; import { AppModule } from '../src/app.module'; import { UsersModule } from '../src/users/users.module'; -const prompt = '\u001b[1G\u001b[0J\u001b[32m>\u001b[0m \u001b[3G'; +const PROMPT = '\u001b[1G\u001b[0J> \u001b[3G'; describe('REPL', () => { beforeEach(() => { @@ -44,7 +44,7 @@ describe('REPL', () => { expect(outputText).to.equal( `UsersService { usersRepository: UsersRepository {} } -${prompt}`, +${PROMPT}`, ); outputText = ''; @@ -52,13 +52,13 @@ ${prompt}`, expect(outputText).to .equal(`\u001b[32m'This action returns all users'\u001b[39m -${prompt}`); +${PROMPT}`); outputText = ''; server.emit('line', 'get(UsersRepository)'); expect(outputText).to.equal(`UsersRepository {} -${prompt}`); +${PROMPT}`); }); it('debug()', async () => { @@ -80,7 +80,7 @@ UsersModule: ◻ UsersService ◻ UsersRepository -${prompt}`, +${PROMPT}`, ); }); @@ -99,7 +99,7 @@ ${prompt}`, Methods: ◻ find -${prompt}`, +${PROMPT}`, ); outputText = ''; @@ -114,7 +114,7 @@ Methods: ◻ update ◻ remove -${prompt}`, +${PROMPT}`, ); }); @@ -135,7 +135,7 @@ ${prompt}`, expect(outputText).to.equal(`${description} Interface: help${signature} -${prompt}`); +${PROMPT}`); }); it(`Typing "get.help" should print function's description and interface`, async () => { @@ -154,7 +154,7 @@ ${prompt}`); expect(outputText).to.equal(`${description} Interface: get${signature} -${prompt}`); +${PROMPT}`); }); it(`Typing "resolve.help" should print function's description and interface`, async () => { @@ -173,7 +173,7 @@ ${prompt}`); expect(outputText).to.equal(`${description} Interface: resolve${signature} -${prompt}`); +${PROMPT}`); }); it(`Typing "select.help" should print function's description and interface`, async () => { @@ -192,7 +192,7 @@ ${prompt}`); expect(outputText).to.equal(`${description} Interface: select${signature} -${prompt}`); +${PROMPT}`); }); it(`Typing "debug.help" should print function's description and interface`, async () => { @@ -211,7 +211,7 @@ ${prompt}`); expect(outputText).to.equal(`${description} Interface: debug${signature} -${prompt}`); +${PROMPT}`); }); it(`Typing "methods.help" should print function's description and interface`, async () => { @@ -230,7 +230,7 @@ ${prompt}`); expect(outputText).to.equal(`${description} Interface: methods${signature} -${prompt}`); +${PROMPT}`); }); }); }); diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts index ff497c0d0d8..1c780ba7bfc 100644 --- a/packages/core/repl/repl.ts +++ b/packages/core/repl/repl.ts @@ -1,5 +1,6 @@ import { Logger, Type } from '@nestjs/common'; import * as _repl from 'repl'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; import { NestFactory } from '../nest-factory'; import { REPL_INITIALIZED_MESSAGE } from './constants'; import { loadNativeFunctionsIntoContext } from './load-native-functions-into-context'; @@ -17,7 +18,7 @@ export async function repl(module: Type) { Logger.log(REPL_INITIALIZED_MESSAGE); const replServer = _repl.start({ - prompt: '\x1b[32m>\x1b[0m ', + prompt: clc.green('> '), ignoreUndefined: true, }); From a2732a4f10caa2f9a20e468fe42a6394d25778a5 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Thu, 2 Jun 2022 18:51:37 -0400 Subject: [PATCH 24/27] fix(core): prevents renaming global providers and modules by marking them as a read-only properties of `globalThis` obj --- packages/core/repl/repl-context.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index cf0e81ce2dc..d1c3bd428bb 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -59,7 +59,6 @@ export class ReplContext { } private initializeContext() { - const globalRef = globalThis; const modules = this.container.getModules(); modules.forEach(moduleRef => { @@ -67,14 +66,18 @@ export class ReplContext { if (moduleName === InternalCoreModule.name) { return; } - if (globalRef[moduleName]) { + if (globalThis[moduleName]) { moduleName += ` (${moduleRef.token})`; } this.introspectCollection(moduleRef, moduleName, 'providers'); this.introspectCollection(moduleRef, moduleName, 'controllers'); - globalRef[moduleName] = moduleRef.metatype; + // For in REPL auto-complete functionality + Object.defineProperty(globalThis, moduleName, { + value: moduleRef.metatype, + configurable: false, + }); }); } @@ -88,12 +91,16 @@ export class ReplContext { const stringifiedToken = this.stringifyToken(token); if ( stringifiedToken === ApplicationConfig.name || - stringifiedToken === moduleRef.metatype.name + stringifiedToken === moduleRef.metatype.name || + globalThis[stringifiedToken] ) { return; } // For in REPL auto-complete functionality - globalThis[stringifiedToken] = token; + Object.defineProperty(globalThis, stringifiedToken, { + value: token, + configurable: false, + }); if (stringifiedToken === ModuleRef.name) { return; From 30618bf703865298ec166845c88d64ccffcda555 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Sat, 4 Jun 2022 18:01:54 -0400 Subject: [PATCH 25/27] feat(core): drop `globalThis` usage from `ReplContext` --- integration/repl/e2e/repl.spec.ts | 5 +- .../load-native-functions-into-context.ts | 33 -------- packages/core/repl/repl-context.ts | 76 +++++++++++++------ packages/core/repl/repl.ts | 14 +++- 4 files changed, 66 insertions(+), 62 deletions(-) delete mode 100644 packages/core/repl/load-native-functions-into-context.ts diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts index c57cb828646..8c20d3a79b7 100644 --- a/integration/repl/e2e/repl.spec.ts +++ b/integration/repl/e2e/repl.spec.ts @@ -12,7 +12,6 @@ import { import { expect } from 'chai'; import * as sinon from 'sinon'; import { AppModule } from '../src/app.module'; -import { UsersModule } from '../src/users/users.module'; const PROMPT = '\u001b[1G\u001b[0J> \u001b[3G'; @@ -28,13 +27,11 @@ describe('REPL', () => { }); afterEach(() => { sinon.restore(); - delete globalThis[AppModule.name]; - delete globalThis[UsersModule.name]; }); it('get()', async () => { const server = await repl(AppModule); - + server.context let outputText = ''; sinon.stub(process.stdout, 'write').callsFake(text => { outputText += text; diff --git a/packages/core/repl/load-native-functions-into-context.ts b/packages/core/repl/load-native-functions-into-context.ts deleted file mode 100644 index 6dd0a5c4e02..00000000000 --- a/packages/core/repl/load-native-functions-into-context.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as _repl from 'repl'; -import { ReplContext } from './repl-context'; -import { ReplFunction } from './repl-function'; -import { ReplFunctionClass } from './repl.interfaces'; - -export function loadNativeFunctionsIntoContext( - replServerContext: _repl.REPLServer['context'], - replContext: ReplContext, -) { - const registerFunctionToReplServerContext = ( - nativeFunction: InstanceType, - ): void => { - // Bind the method to REPL's context: - replServerContext[nativeFunction.fnDefinition.name] = - nativeFunction.action.bind(nativeFunction); - - // Load the help trigger as a `help` getter on each native function: - const functionBoundRef: ReplFunction['action'] = - replServerContext[nativeFunction.fnDefinition.name]; - Object.defineProperty(functionBoundRef, 'help', { - enumerable: false, - configurable: false, - get: () => - // Dynamically builds the help message as will unlikely to be called - // several times. - replContext.writeToStdout(nativeFunction.makeHelpMessage()), - }); - }; - - for (const [, nativeFunction] of replContext.nativeFunctions) { - registerFunctionToReplServerContext(nativeFunction); - } -} diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index d1c3bd428bb..7a8696cae40 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -11,6 +11,7 @@ import { ResolveReplFn, SelectReplFn, } from './native-functions'; +import { ReplFunction } from './repl-function'; import type { ReplFunctionClass } from './repl.interfaces'; type ModuleKey = string; @@ -19,9 +20,12 @@ export type ModuleDebugEntry = { providers: Record; }; +type ReplScope = Record; + export class ReplContext { public readonly logger = new Logger(ReplContext.name); public debugRegistry: Record = {}; + public readonly globalScope: ReplScope = Object.create(null); public readonly nativeFunctions = new Map< string, InstanceType @@ -33,6 +37,7 @@ export class ReplContext { nativeFunctionsClassRefs?: ReplFunctionClass[], ) { this.container = (app as any).container; // Using `any` because `app.container` is not public. + this.initializeContext(); this.initializeNativeFunctions(nativeFunctionsClassRefs || []); } @@ -41,23 +46,6 @@ export class ReplContext { process.stdout.write(text); } - public addNativeFunction(NativeFunction: ReplFunctionClass): void { - const nativeFunction = new NativeFunction(this); - - this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction); - - nativeFunction.fnDefinition.aliases?.forEach(aliaseName => { - const aliasNativeFunction: InstanceType = - Object.create(nativeFunction); - aliasNativeFunction.fnDefinition = { - name: aliaseName, - description: aliasNativeFunction.fnDefinition.description, - signature: aliasNativeFunction.fnDefinition.signature, - }; - this.nativeFunctions.set(aliaseName, aliasNativeFunction); - }); - } - private initializeContext() { const modules = this.container.getModules(); @@ -66,7 +54,7 @@ export class ReplContext { if (moduleName === InternalCoreModule.name) { return; } - if (globalThis[moduleName]) { + if (this.globalScope[moduleName]) { moduleName += ` (${moduleRef.token})`; } @@ -74,9 +62,10 @@ export class ReplContext { this.introspectCollection(moduleRef, moduleName, 'controllers'); // For in REPL auto-complete functionality - Object.defineProperty(globalThis, moduleName, { + Object.defineProperty(this.globalScope, moduleName, { value: moduleRef.metatype, configurable: false, + enumerable: true, }); }); } @@ -92,14 +81,15 @@ export class ReplContext { if ( stringifiedToken === ApplicationConfig.name || stringifiedToken === moduleRef.metatype.name || - globalThis[stringifiedToken] + this.globalScope[stringifiedToken] ) { return; } // For in REPL auto-complete functionality - Object.defineProperty(globalThis, stringifiedToken, { + Object.defineProperty(this.globalScope, stringifiedToken, { value: token, configurable: false, + enumerable: true, }); if (stringifiedToken === ModuleRef.name) { @@ -122,6 +112,47 @@ export class ReplContext { : token; } + private addNativeFunction( + NativeFunctionRef: ReplFunctionClass, + ): InstanceType { + const nativeFunction = new NativeFunctionRef(this); + + this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction); + + nativeFunction.fnDefinition.aliases?.forEach(aliaseName => { + const aliasNativeFunction: InstanceType = + Object.create(nativeFunction); + aliasNativeFunction.fnDefinition = { + name: aliaseName, + description: aliasNativeFunction.fnDefinition.description, + signature: aliasNativeFunction.fnDefinition.signature, + }; + this.nativeFunctions.set(aliaseName, aliasNativeFunction); + }); + + return nativeFunction; + } + + private registerFunctionIntoGlobalScope( + nativeFunction: InstanceType, + ) { + // Bind the method to REPL's context: + this.globalScope[nativeFunction.fnDefinition.name] = + nativeFunction.action.bind(nativeFunction); + + // Load the help trigger as a `help` getter on each native function: + const functionBoundRef: ReplFunction['action'] = + this.globalScope[nativeFunction.fnDefinition.name]; + Object.defineProperty(functionBoundRef, 'help', { + enumerable: false, + configurable: false, + get: () => + // Dynamically builds the help message as will unlikely to be called + // several times. + this.writeToStdout(nativeFunction.makeHelpMessage()), + }); + } + private initializeNativeFunctions( nativeFunctionsClassRefs: ReplFunctionClass[], ): void { @@ -137,7 +168,8 @@ export class ReplContext { builtInFunctionsClassRefs .concat(nativeFunctionsClassRefs) .forEach(NativeFunction => { - this.addNativeFunction(NativeFunction); + const nativeFunction = this.addNativeFunction(NativeFunction); + this.registerFunctionIntoGlobalScope(nativeFunction); }); } } diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts index 1c780ba7bfc..14fa6575943 100644 --- a/packages/core/repl/repl.ts +++ b/packages/core/repl/repl.ts @@ -3,10 +3,19 @@ import * as _repl from 'repl'; import { clc } from '@nestjs/common/utils/cli-colors.util'; import { NestFactory } from '../nest-factory'; import { REPL_INITIALIZED_MESSAGE } from './constants'; -import { loadNativeFunctionsIntoContext } from './load-native-functions-into-context'; import { ReplContext } from './repl-context'; import { ReplLogger } from './repl-logger'; +function copyInto(target, source): void { + Object.defineProperties( + target, + Object.keys(source).reduce((descriptors, key) => { + descriptors[key] = Object.getOwnPropertyDescriptor(source, key); + return descriptors; + }, Object.create(null)), + ); +} + export async function repl(module: Type) { const app = await NestFactory.create(module, { abortOnError: false, @@ -21,8 +30,7 @@ export async function repl(module: Type) { prompt: clc.green('> '), ignoreUndefined: true, }); - - loadNativeFunctionsIntoContext(replServer.context, replContext); + copyInto(replServer.context, replContext.globalScope); return replServer; } From 2c4aa9f0b4766fd1604d43c1e11a6b848b98e480 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Sat, 4 Jun 2022 18:43:06 -0400 Subject: [PATCH 26/27] refactor(core): fix typo on spec files name --- .../{debug-relp-fn.spec.ts => debug-repl-fn.spec.ts} | 0 .../native-functions/{get-relp-fn.spec.ts => get-repl-fn.spec.ts} | 0 .../{help-relp-fn.spec.ts => help-repl-fn.spec.ts} | 0 .../{methods-relp-fn.spec.ts => methods-repl-fn.spec.ts} | 0 .../{resolve-relp-fn.spec.ts => resolve-repl-fn.spec.ts} | 0 .../{select-relp-fn.spec.ts => select-repl-fn.spec.ts} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename packages/core/test/repl/native-functions/{debug-relp-fn.spec.ts => debug-repl-fn.spec.ts} (100%) rename packages/core/test/repl/native-functions/{get-relp-fn.spec.ts => get-repl-fn.spec.ts} (100%) rename packages/core/test/repl/native-functions/{help-relp-fn.spec.ts => help-repl-fn.spec.ts} (100%) rename packages/core/test/repl/native-functions/{methods-relp-fn.spec.ts => methods-repl-fn.spec.ts} (100%) rename packages/core/test/repl/native-functions/{resolve-relp-fn.spec.ts => resolve-repl-fn.spec.ts} (100%) rename packages/core/test/repl/native-functions/{select-relp-fn.spec.ts => select-repl-fn.spec.ts} (100%) diff --git a/packages/core/test/repl/native-functions/debug-relp-fn.spec.ts b/packages/core/test/repl/native-functions/debug-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/debug-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/debug-repl-fn.spec.ts diff --git a/packages/core/test/repl/native-functions/get-relp-fn.spec.ts b/packages/core/test/repl/native-functions/get-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/get-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/get-repl-fn.spec.ts diff --git a/packages/core/test/repl/native-functions/help-relp-fn.spec.ts b/packages/core/test/repl/native-functions/help-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/help-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/help-repl-fn.spec.ts diff --git a/packages/core/test/repl/native-functions/methods-relp-fn.spec.ts b/packages/core/test/repl/native-functions/methods-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/methods-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/methods-repl-fn.spec.ts diff --git a/packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts b/packages/core/test/repl/native-functions/resolve-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/resolve-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/resolve-repl-fn.spec.ts diff --git a/packages/core/test/repl/native-functions/select-relp-fn.spec.ts b/packages/core/test/repl/native-functions/select-repl-fn.spec.ts similarity index 100% rename from packages/core/test/repl/native-functions/select-relp-fn.spec.ts rename to packages/core/test/repl/native-functions/select-repl-fn.spec.ts From 1cc12acaf6b7172174191efcfa8d3ce709756751 Mon Sep 17 00:00:00 2001 From: "Micael Levi (@micalevisk)" Date: Tue, 14 Jun 2022 08:48:35 -0400 Subject: [PATCH 27/27] refactor(repl): extract utility from repl main file --- packages/core/repl/assign-to-object.util.ts | 14 ++++++++ packages/core/repl/repl.ts | 13 ++----- .../test/repl/assign-to-object.util.spec.ts | 36 +++++++++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 packages/core/repl/assign-to-object.util.ts create mode 100644 packages/core/test/repl/assign-to-object.util.spec.ts diff --git a/packages/core/repl/assign-to-object.util.ts b/packages/core/repl/assign-to-object.util.ts new file mode 100644 index 00000000000..16f9379992f --- /dev/null +++ b/packages/core/repl/assign-to-object.util.ts @@ -0,0 +1,14 @@ +/** + * Similar to `Object.assign` but copying properties descriptors from `source` + * as well. + */ +export function assignToObject(target: T, source: U): T & U { + Object.defineProperties( + target, + Object.keys(source).reduce((descriptors, key) => { + descriptors[key] = Object.getOwnPropertyDescriptor(source, key); + return descriptors; + }, Object.create(null)), + ); + return target as T & U; +} diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts index 14fa6575943..079f16f1fe2 100644 --- a/packages/core/repl/repl.ts +++ b/packages/core/repl/repl.ts @@ -5,16 +5,7 @@ import { NestFactory } from '../nest-factory'; import { REPL_INITIALIZED_MESSAGE } from './constants'; import { ReplContext } from './repl-context'; import { ReplLogger } from './repl-logger'; - -function copyInto(target, source): void { - Object.defineProperties( - target, - Object.keys(source).reduce((descriptors, key) => { - descriptors[key] = Object.getOwnPropertyDescriptor(source, key); - return descriptors; - }, Object.create(null)), - ); -} +import { assignToObject } from './assign-to-object.util'; export async function repl(module: Type) { const app = await NestFactory.create(module, { @@ -30,7 +21,7 @@ export async function repl(module: Type) { prompt: clc.green('> '), ignoreUndefined: true, }); - copyInto(replServer.context, replContext.globalScope); + assignToObject(replServer.context, replContext.globalScope); return replServer; } diff --git a/packages/core/test/repl/assign-to-object.util.spec.ts b/packages/core/test/repl/assign-to-object.util.spec.ts new file mode 100644 index 00000000000..812343aef90 --- /dev/null +++ b/packages/core/test/repl/assign-to-object.util.spec.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import { assignToObject } from '@nestjs/core/repl/assign-to-object.util'; + +describe('assignToObject', () => { + it('should copy all enumerable properties and their descriptors', () => { + const sourceObj = {}; + Object.defineProperty(sourceObj, 'foo', { + value: 123, + configurable: true, + enumerable: true, + writable: true, + }); + Object.defineProperty(sourceObj, 'bar', { + value: 456, + configurable: true, + enumerable: true, + writable: false, + }); + const targetObj = {}; + + assignToObject(targetObj, sourceObj); + + expect(Object.getOwnPropertyDescriptor(targetObj, 'foo')).to.be.eql({ + value: 123, + configurable: true, + enumerable: true, + writable: true, + }); + expect(Object.getOwnPropertyDescriptor(targetObj, 'bar')).to.be.eql({ + value: 456, + configurable: true, + enumerable: true, + writable: false, + }); + }); +});