Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): read–eval–print loop feature #9684

Merged
merged 30 commits into from Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
245ccd1
feat(core): repl
kamilmysliwiec May 29, 2022
bae30b2
test(): remove unnecessary files
kamilmysliwiec May 29, 2022
21d6dd1
lint(core): address linter errors
kamilmysliwiec May 29, 2022
a5ecaeb
feat(common): add bold coloring to cli colors utils
micalevisk May 31, 2022
f37e5d5
feat(core): add help messages to built-in repl functions
micalevisk May 31, 2022
1b95a46
feat(core): add `help` native function to repl
micalevisk May 31, 2022
8282d8f
refactor(core): move each repl function to their own file
micalevisk May 31, 2022
964d02d
refactor(core): rename `ReplContext#initialize` to `initializeContext`
micalevisk May 31, 2022
d38a4e6
refactor(core): extract `loadNativeFunctionsIntoContext` from `repl.ts`
micalevisk May 31, 2022
fbc0ab8
refactor(core): clean-up `ReplContext` fields
micalevisk May 31, 2022
092f350
refactor(core): replace array by map for `nativeFunctions` field
micalevisk May 31, 2022
32c0a74
feat(core): add description to `debug` native repl function
micalevisk May 31, 2022
59965cf
test(core,integration): fix repl test suite for the new api
micalevisk May 31, 2022
5ab8800
fix(core): add missing return to `GetReplFn#action`
micalevisk May 31, 2022
833d16e
test(core): add few missing tests for repl scope
micalevisk Jun 1, 2022
53fc03a
Update packages/core/repl/native-functions/debug-repl-fn.ts
kamilmysliwiec Jun 1, 2022
89b39f9
Update packages/core/repl/native-functions/debug-repl-fn.ts
kamilmysliwiec Jun 1, 2022
160b521
Update packages/core/repl/native-functions/methods-repl-fn.ts
kamilmysliwiec Jun 1, 2022
99c6c62
Update packages/core/repl/native-functions/resolve-repl-fn.ts
kamilmysliwiec Jun 1, 2022
c18f4b9
Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts
kamilmysliwiec Jun 1, 2022
b8e4863
Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts
kamilmysliwiec Jun 1, 2022
09d6c4b
Update packages/core/test/repl/native-functions/help-relp-fn.spec.ts
kamilmysliwiec Jun 1, 2022
9fd45a9
Merge pull request #9695 from micalevisk/feat/repl-improvements-v2
kamilmysliwiec Jun 1, 2022
d8c8e67
fix(core): prompt indicador respect `NO_COLOR` config
micalevisk Jun 2, 2022
018fd6b
Merge pull request #9715 from micalevisk/feat/repl-fix-coloring
kamilmysliwiec Jun 2, 2022
a2732a4
fix(core): prevents renaming global providers and modules
micalevisk Jun 2, 2022
30618bf
feat(core): drop `globalThis` usage from `ReplContext`
micalevisk Jun 4, 2022
2c4aa9f
refactor(core): fix typo on spec files name
micalevisk Jun 4, 2022
1cc12ac
refactor(repl): extract utility from repl main file
micalevisk Jun 14, 2022
6843117
Merge pull request #9720 from micalevisk/feat/repl-fix-injections
kamilmysliwiec Jun 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
233 changes: 233 additions & 0 deletions integration/repl/e2e/repl.spec.ts
@@ -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
@@ -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
@@ -0,0 +1 @@
export class CreateUserDto {}
4 changes: 4 additions & 0 deletions 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) {}
1 change: 1 addition & 0 deletions integration/repl/src/users/entities/user.entity.ts
@@ -0,0 +1 @@
export class User {}
34 changes: 34 additions & 0 deletions 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);
}
}
16 changes: 16 additions & 0 deletions 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 {}
8 changes: 8 additions & 0 deletions 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' }];
}
}
32 changes: 32 additions & 0 deletions 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`;
}
}
22 changes: 22 additions & 0 deletions 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"
]
}
1 change: 1 addition & 0 deletions packages/common/utils/cli-colors.util.ts
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
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
@@ -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;
}