Skip to content

Commit

Permalink
Merge pull request #9684 from nestjs/feat/repl
Browse files Browse the repository at this point in the history
feat(core): read–eval–print loop feature
  • Loading branch information
kamilmysliwiec committed Jun 15, 2022
2 parents cdb3043 + 6843117 commit b29d88f
Show file tree
Hide file tree
Showing 35 changed files with 1,357 additions and 0 deletions.
233 changes: 233 additions & 0 deletions integration/repl/e2e/repl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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';

const PROMPT = '\u001b[1G\u001b[0J> \u001b[3G';

describe('REPL', () => {
beforeEach(() => {
// 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();
});

it('get()', async () => {
const server = await repl(AppModule);
server.context
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}`,
);
});

describe('<native_function>.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}`);
});
});
});
7 changes: 7 additions & 0 deletions integration/repl/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
imports: [UsersModule],
})
export class AppModule {}
1 change: 1 addition & 0 deletions integration/repl/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class CreateUserDto {}
4 changes: 4 additions & 0 deletions integration/repl/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}
1 change: 1 addition & 0 deletions integration/repl/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class User {}
34 changes: 34 additions & 0 deletions integration/repl/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 16 additions & 0 deletions integration/repl/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
8 changes: 8 additions & 0 deletions integration/repl/src/users/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersRepository {
async find() {
return [{ id: 1, email: 'test@nestjs.com' }];
}
}
32 changes: 32 additions & 0 deletions integration/repl/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
}
22 changes: 22 additions & 0 deletions integration/repl/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
1 change: 1 addition & 0 deletions packages/common/utils/cli-colors.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
14 changes: 14 additions & 0 deletions packages/core/repl/assign-to-object.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Similar to `Object.assign` but copying properties descriptors from `source`
* as well.
*/
export function assignToObject<T, U>(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;
}

0 comments on commit b29d88f

Please sign in to comment.