-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; |
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); | ||
}; | ||
} |
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[]; | ||
} |
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, { | ||
|
@@ -12,6 +53,7 @@ export async function repl(module: Type) { | |
}); | ||
await app.init(); | ||
|
||
const ReplContext = CoreReplContext; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now we can parametrize this 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 |
||
const replContext = new ReplContext(app); | ||
Logger.log(REPL_INITIALIZED_MESSAGE); | ||
|
||
|
@@ -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; | ||
} |
There was a problem hiding this comment.
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
functionThere was a problem hiding this comment.
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.