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,common): add helpers messages to REPL built-in functions #9692

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
6 changes: 6 additions & 0 deletions packages/core/repl/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export const REPL_INITIALIZED_MESSAGE = 'REPL initialized';

/**
* NestJS core REPL metadata's identifier.
* The metadata itself will have the shape of `ReplMetadata` interface.
*/
export const REPL_METADATA_KEY = 'repl:metadata';
84 changes: 74 additions & 10 deletions packages/core/repl/repl-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DynamicModule,
INestApplication,
INestApplicationContext,
InjectionToken,
Logger,
Type,
Expand All @@ -11,6 +12,12 @@ import { ModuleRef, NestContainer } from '../injector';
import { InternalCoreModule } from '../injector/internal-core-module';
import { Module } from '../injector/module';
import { MetadataScanner } from '../metadata-scanner';
import { makeReplFnOpt, ReplFn } from './repl-fn.decorator';
import { REPL_METADATA_KEY } from './constants';
import type {
ReplMetadata,
ReplNativeFunctionMetadata,
} from './repl.interfaces';

type ModuleKey = string;
type ModuleDebugEntry = {
Expand All @@ -29,23 +36,74 @@ export class ReplContext {
this.initialize();
}

$(token: string | symbol | Function | Type<any>) {
return this.get(token);
@ReplFn(
makeReplFnOpt('Display all available REPL native functions.', '() => void'),
)
help(): void {
const buildHelpMessage = ({
name,
description,
}: ReplNativeFunctionMetadata) =>
clc.cyanBright(name) +
(description ? ` ${clc.bold('-')} ${description}` : '');

const replMetadata: ReplMetadata = Reflect.getMetadata(
REPL_METADATA_KEY,
ReplContext,
);
const sortedNativeFunctions = replMetadata.nativeFunctions.sort((a, b) =>
a.name < b.name ? -1 : 1,
);
this.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',
);
}

get(token: string | symbol | Function | Type<any>) {
@ReplFn(
makeReplFnOpt(
'Retrieves an instance of either injectable or controller, otherwise, throws exception.',
'(token: InjectionToken) => any',
),
)
get(token: string | symbol | Function | Type<any>): any {
return this.app.get(token);
}

resolve(token: string | symbol | Function | Type<any>, contextId: any) {
@ReplFn({ aliasOf: 'get' })
$(...args: Parameters<ReplContext['get']>) {
return this.get(...args);
}

@ReplFn(
makeReplFnOpt(
'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception',
'(token: InjectionToken, contextId: any) => Promise<any>',
),
)
resolve(
token: string | symbol | Function | Type<any>,
contextId: any,
): Promise<any> {
return this.app.resolve(token, contextId);
}

select(token: DynamicModule | Type<unknown>) {
@ReplFn(
makeReplFnOpt(
'Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.',
'(token: DynamicModule | ClassRef) => INestApplicationContext',
),
)
select(token: DynamicModule | Type<unknown>): INestApplicationContext {
return this.app.select(token);
}

debug(moduleCls?: Type | string) {
@ReplFn(makeReplFnOpt('', '(moduleCls?: ClassRef | string) => void'))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kamilmysliwiec please help me on writting a description for the debug function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@micalevisk, an idea but take it as a "draft":

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(moduleCls?: Type | string): void {
this.writeToStdout('\n');

if (moduleCls) {
Expand All @@ -66,6 +124,12 @@ export class ReplContext {
this.writeToStdout('\n');
}

@ReplFn(
makeReplFnOpt(
'Display all public methods available on a given provider.',
'(token: ClassRef | string) => void',
),
)
methods(token: Type | string) {
const proto =
typeof token !== 'function'
Expand All @@ -84,6 +148,10 @@ export class ReplContext {
this.writeToStdout('\n');
}

writeToStdout(text: string) {
process.stdout.write(text);
}

private initialize() {
const globalRef = globalThis;
const modules = this.container.getModules();
Expand Down Expand Up @@ -160,8 +228,4 @@ export class ReplContext {
printCollection('controllers');
printCollection('providers');
}

private writeToStdout(text: string) {
process.stdout.write(text);
}
}
81 changes: 81 additions & 0 deletions packages/core/repl/repl-fn.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { isSymbol, isUndefined } from '@nestjs/common/utils/shared.utils';
import { REPL_METADATA_KEY } from './constants';
import type {
ReplMetadata,
ReplNativeFunctionMetadata,
} from './repl.interfaces';

type ReplFnDefinition = {
/** Function's description to display when `<function>.help` is entered. */
fnDescription: string;

/**
* Function's signature following TypeScript _function type expression_ syntax.
* @example '(token: InjectionToken) => any'
*/
fnSignature: ReplNativeFunctionMetadata['signature'];
};

type ReplFnAliasDefinition = {
/**
* When the function is just an alias to another one that was registered
* already. Note that the function with the name passed to `aliasOf` should
* appear before this one on class methods's definition.
*/
aliasOf: string;
};

export const makeReplFnOpt = (
description: string,
signature: string,
): { fnDescription: string; fnSignature: string } => ({
fnDescription: description,
fnSignature: signature,
});

export function ReplFn(replFnOpts: ReplFnAliasDefinition): MethodDecorator;
export function ReplFn(replFnOpts: ReplFnDefinition): MethodDecorator;
export function ReplFn(
replFnOpts: ReplFnDefinition | ReplFnAliasDefinition,
): MethodDecorator {
return (property: Object, methodName: string | symbol) => {
// As we are using class's properties as the name of the global function to
// avoid naming clashes, we won't allow symbols.
if (isSymbol(methodName)) {
return;
}

const ClassRef = property.constructor;
const replMetadata: ReplMetadata = Reflect.getMetadata(
REPL_METADATA_KEY,
ClassRef,
) || { nativeFunctions: [] };

if ('aliasOf' in replFnOpts) {
if (replFnOpts.aliasOf) {
const nativeFunction = replMetadata.nativeFunctions.find(
({ name }) => name === replFnOpts.aliasOf,
);

// If the native function was registered
if (!isUndefined(nativeFunction)) {
replMetadata.nativeFunctions.push({
name: methodName,
description: nativeFunction.description,
signature: nativeFunction.signature,
});
Reflect.defineMetadata(REPL_METADATA_KEY, replMetadata, ClassRef);
}
}

return;
}

replMetadata.nativeFunctions.push({
name: methodName,
description: replFnOpts.fnDescription,
signature: replFnOpts.fnSignature,
});
Reflect.defineMetadata(REPL_METADATA_KEY, replMetadata, ClassRef);
};
}
25 changes: 25 additions & 0 deletions packages/core/repl/repl.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Metadata of bult-in functions that will be available on the global context of
* the NestJS Read-Eval-Print-Loop (REPL).
*/
export interface ReplNativeFunctionMetadata {
/** Function's name. */
name: string;

/** Function's description to display when `<function>.help` is entered. */
description: string;

/**
* Function's signature following TypeScript _function type expression_ syntax,
* to display when `<function>.help` is entered along with function's description.
* @example '(token: InjectionToken) => any'
*/
signature: string;
}

/**
* Metadata attached to REPL context class.
*/
export interface ReplMetadata {
nativeFunctions: ReplNativeFunctionMetadata[];
}
57 changes: 49 additions & 8 deletions packages/core/repl/repl.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
import { Logger, Type } from '@nestjs/common';
import { clc } from '@nestjs/common/utils/cli-colors.util';
import * as _repl from 'repl';
import { NestFactory } from '../nest-factory';
import { REPL_INITIALIZED_MESSAGE } from './constants';
import { ReplContext } from './repl-context';
import { REPL_INITIALIZED_MESSAGE, REPL_METADATA_KEY } from './constants';
import { ReplContext as CoreReplContext, ReplContext } from './repl-context';
import { ReplLogger } from './repl-logger';
import { ReplMetadata } from './repl.interfaces';

/** Utility to build help messages for NestJS REPL native functions. */
const makeHelpMessage = (
description: string,
fnSignatureWithName: string,
): string =>
`${clc.yellow(description)}\n${clc.magentaBright('Interface:')} ${clc.bold(
fnSignatureWithName,
)}\n`;

function loadNativeFunctionsOnContext(
replServerContext: _repl.REPLServer['context'],
replContext: ReplContext,
replMetadata: ReplMetadata,
) {
replMetadata.nativeFunctions.forEach(nativeFunction => {
const functionRef: Function = replContext[nativeFunction.name];
if (!functionRef) return;

// Bind the method to REPL's context:
const functionBoundRef = (replServerContext[nativeFunction.name] =
functionRef.bind(replContext));

// Load the help trigger as a `help` getter on each native function:
Object.defineProperty(functionBoundRef, 'help', {
enumerable: false,
configurable: false,
get: () => {
// Lazy building the help message as will unlikely to be called often,
// and we can keep the closure context smaller just for this task.
const helpMessage = makeHelpMessage(
nativeFunction.description,
`${nativeFunction.name}${nativeFunction.signature}`,
);
replContext.writeToStdout(helpMessage);
},
});
});
}

export async function repl(module: Type) {
const app = await NestFactory.create(module, {
Expand All @@ -12,6 +53,7 @@ export async function repl(module: Type) {
});
await app.init();

const ReplContext = CoreReplContext;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we can parametrize this ReplContext variable (so users could write their own 'native functions'), falling back to CoreReplContext

I didn't implement that feature in this PR because I think it would be better to wait for requests instead of implementing something that no one will use. Also because the interface of ReplContext might not be well defined yet

const replContext = new ReplContext(app);
Logger.log(REPL_INITIALIZED_MESSAGE);

Expand All @@ -20,12 +62,11 @@ 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);
const replMetadata: ReplMetadata = Reflect.getMetadata(
REPL_METADATA_KEY,
ReplContext,
);
loadNativeFunctionsOnContext(replServer.context, replContext, replMetadata);

return replServer;
}