• Stars
    star
    429
  • Rank 101,271 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 4 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A module for using NestJS to build up CLI applications

CI Commitizen friendly Coffee codebeat badge

Nest Commander Logo

NestJS Commander

Have you been building amazing applications with NestJS? Do you want that same structure for absolutely everything you're working with? Have you always wanted to build up some sweet CLI application but don't really know where to start? This is the solution. A package to bring building CLI applications to the Nest world with the same structure that you already know and love โค๏ธ Built on top of the popular Commander package.

Installation

Before you get started, you'll need to install a few packages. First and foremost, this one: nest-commander (name pending). You'll also need to install @nestjs/common and @nestjs/core as this package makes use of them under the hood, but doesn't want to tie you down to a specific version, yay peerDependencies!

npm i nest-commander @nestjs/common @nestjs/core
# OR
yarn add nest-commander @nestjs/common @nestjs/core
# OR
pnpm i nest-commander @nestjs/common @nestjs/core

A Command File

nest-commander makes it easy to write new command line applications with decorators via the @Command() decorator for classes and the @Option() decorator for methods of that class. Every command file should implement the CommandRunner abstract class and should be decorated with a @Command() decorator.

CommandRunner

Every command is seen as an @Injectable() by Nest, so your normal Dependency Injection still works as you would expect it to (woohoo!). The only thing to take note of is the abstract class CommandRunner, which should be implemented by each command. The CommandRunner abstract class ensures that all commands have a run method that return a Promise<void> and takes in the parameters string[], Record<string, any>. The run command is where you can kick all of your logic off from, it will take in whatever parameters did not match option flags and pass them in as an array, just in case you are really meaning to work with multiple parameters. As for the options, the Record<string, any>, the names of these properties match the name property given to the @Option() decorators, while their value matches the return of the option handler. If you'd like better type safety, you are welcome to create an interface for your options as well. You can view how the Basic Command test manages that if interested.

@Command()

The @Command() decorator is to define what CLI command the class is going to manage and take care of. The decorator takes in an object to define properties of the command. The options passed here would be the same as the options passed to a new command for Commander

property type required description
name string true the name of the command
arguments string false Named arguments for the command to work with. These can be required <> or optional [], but do not map to an option like a flag does
description string false the description of the command. This will be used by the --help or -h flags to have a formalized way of what to print out
argsDescription Record<string, string> false An object containing the description of each argument. This will be used by -h or --help
Options CommandOptions false Extra options to pass on down to commander

For mor information on the @Command() and @Option() parameters, check out the Commander docs.

@Option()

Often times you're not just running a single command with a single input, but rather you're running a command with multiple options and flags. Think of something like git commit: you can pass a --amend flag, a -m flag, or even -a, all of these change how the command runs. These flags are able to be set for each command using the @Option() decorator on a method for how that flag should be parsed. Do note that every command sent in via the command line is a raw string, so if you need to transform that string to a number or a boolean, or any other type, this handler is where it can be done. See the putting it all together for an example. The @Option() decorator, like the @Command() one, takes in an object of options defined in the table below

property type required description
flags string true a string that represents the option's incoming flag and if the option is required (using <>) or optional (using [])
description string false the description of the option, used if adding a --help flag
defaultValue string or boolean false the default value for the flag

Under the hood, the method that the@Option() is decorating is the custom parser passed to commander for how the value should be parsed. This means if you want to parse a boolean value, the best way to do so would be to use JSON.parse(val) as Boolean('false') actually returns true instead of the expected false.

Running the Command

Similar to how in a NestJS application we can use the NestFactory to create a server for us, and run it using listen, the nest-commander package exposes a simple to use API to run your server. Import the CommandFactory and use the static method run and pass in the root module of your application. This would probably look like below

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

By default, Nest's logger is disabled when using the CommandFactory. It's possible to provide it though, as the second argument to the run function. You can either provide a custom NestJS logger, or an array of log levels you want to keep - it might be useful to at least provide ['error'] here, if you only want to print out Nest's error logs.

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';
import { LogService } './log.service';

async function bootstrap() {
  await CommandFactory.run(AppModule, new LogService());

  // or, if you only want to print Nest's warnings and errors
  await CommandFactory.run(AppModule, ['warn', 'error']);
}

bootstrap();

And that's it. Under the hood, CommandFactory will worry about calling NestFactory for you and calling app.close() when necessary, so you shouldn't need to worry about memory leaks there. If you need to add in some error handling, there's always try/catch wrapping the run command, or you can chain on some .catch() method to the bootstrap() call.

Testing

So what's the use of writing a super awesome command line script if you can't test it super easily, right? Fortunately, nest-commander has some utilities you can make use of that fits in perfectly with the NestJS ecosystem, it'll feel right at home to any Nestlings out there. Instead of using the CommandFactory for building the command in test mode, you can use CommandTestFactory and pass in your metadata, very similarly to how Test.createTestingModule from @nestjs/testing works. In fact, it uses this package under the hood. You're also still able to chain on the overrideProvider methods before calling compile() so you can swap out DI pieces right in the test. A nice example of this can be seen in the basic.command.factory.spec.ts file.

Putting it All Together

The following class would equate to having a CLI command that can take in the subcommand basic or be called directly, with -n, -s, and -b (along with their long flags) all being supported and with custom parsers for each option. The --help flag is also supported, as is customary with commander.

import { Command, CommandRunner, Option } from 'nest-commander';
import { LogService } from './log.service';

interface BasicCommandOptions {
  string?: string;
  boolean?: boolean;
  number?: number;
}

@Command({ name: 'basic', description: 'A parameter parse' })
export class BasicCommand extends CommandRunner {
  constructor(private readonly logService: LogService) {
    super();
  }

  async run(passedParam: string[], options?: BasicCommandOptions): Promise<void> {
    if (options?.boolean !== undefined && options?.boolean !== null) {
      this.runWithBoolean(passedParam, options.boolean);
    } else if (options?.number) {
      this.runWithNumber(passedParam, options.number);
    } else if (options?.string) {
      this.runWithString(passedParam, options.string);
    } else {
      this.runWithNone(passedParam);
    }
  }

  @Option({
    flags: '-n, --number [number]',
    description: 'A basic number parser'
  })
  parseNumber(val: string): number {
    return Number(val);
  }

  @Option({
    flags: '-s, --string [string]',
    description: 'A string return'
  })
  parseString(val: string): string {
    return val;
  }

  @Option({
    flags: '-b, --boolean [boolean]',
    description: 'A boolean parser'
  })
  parseBoolean(val: string): boolean {
    return JSON.parse(val);
  }

  runWithString(param: string[], option: string): void {
    this.logService.log({ param, string: option });
  }

  runWithNumber(param: string[], option: number): void {
    this.logService.log({ param, number: option });
  }

  runWithBoolean(param: string[], option: boolean): void {
    this.logService.log({ param, boolean: option });
  }

  runWithNone(param: string[]): void {
    this.logService.log({ param });
  }
}

Make sure the command class is added to a module

@Module({
  providers: [LogService, BasicCommand]
})
export class AppModule {}

And now to be able to run the CLI in your main.ts you can do the following

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

And just like that, you've got a command line application.

License

This project is licensed under the MIT License - see the LICENSE file for details.

More Repositories

1

testing-nestjs

A repository to show off to the community methods of testing NestJS including Unit Tests, Integration Tests, E2E Tests, pipes, filters, interceptors, GraphQL, Mongo, TypeORM, and more!
TypeScript
2,892
star
2

nestjs-spelunker

A NestJS Module for generating a NestJS Applications Module Dependency Graph.
TypeScript
306
star
3

ogma

A monorepo for the ogma logger and related packages
TypeScript
292
star
4

zeldaPlay

A Single Page Application to help zeldaPlay players to track their characters and progress
TypeScript
115
star
5

nest-lab

A repository to hold "experimental" packages for Nest. Honestly, I'm just tired of not having a good scope to put packages under ๐Ÿ˜ธ
TypeScript
85
star
6

nest-docker-template

Small template for using NestJS and Docker together
TypeScript
43
star
7

unteris

Unteris is a TTRP homebrew setting filled with cults, ancient magic, and deities. This is the source code for the website and server
TypeScript
40
star
8

nest-samples

A repository to hold a bunch of Nest samples, cause I'm tired of remaking them and losing them. Now it's just a sinlge repo to hold them all
TypeScript
39
star
9

nest-e2e-sample

An E2E testing sample using uvu and pactumJS to test a NestJS application
TypeScript
28
star
10

retryable-http-nestjs

A small module to show off failing httpService calls in Nest and what you can do to retry them
TypeScript
25
star
11

wtf-is-a-minimum-reproduction

A Description of what the hell a minimum reproduction really is
23
star
12

nest-cookies

A repository to hold the nest-cookies package. This will be a placeholder until I hear back from the nestjsplus/cookies owner
TypeScript
17
star
13

nestjs-oauth

An OAuth Module the mimics Passport's functionality without some of passport's wonkiness
TypeScript
17
star
14

dev.to

A repo for holding my dev.to posts, to allow me to keep them n source control and easily deploy them
TypeScript
16
star
15

fastify-jwt

A sample application to show Fastify plus JWT with Nest without Passport
TypeScript
14
star
16

ogma-depricated

A simple, no-nonsense logging package for NodeJS
TypeScript
10
star
17

nestjs-graphql-e2e-plugin

A repo to show how the NestJS GraphQL CLI Plugin can be used in E2E tests with Jest.
TypeScript
10
star
18

dynamic-module-test

Small repository that reproduces an issue in my main project for testability sake.
TypeScript
9
star
19

nest-content-negotiation

A small server to show off a content negotiation interceptor
TypeScript
7
star
20

nx-uvu

An nx executor for the uvu test library
TypeScript
6
star
21

nestjs-dynamic-render

A repository to show a way to dynamically render a template without using Nest's `@Render()` decoraotr
TypeScript
6
star
22

nestjs-optional-body

A small repo to show how it is possible to use optional bodies and have them validated by class-validator with NestJS
TypeScript
5
star
23

DnDCharacters

Just a repository to hold my plain text D&D Characters, so I can access them practically anywhere
Shell
3
star
24

context-fastify-middleware

A simple minimum reproduction to show that middleware run slightly differnetly inside of Nest for Fastify and Express
TypeScript
3
star
25

nestjs-chained-interceptor

A repo to show how interceptors can be chained together
TypeScript
3
star
26

dotfiles

Making a repo for dotfiles, managed by chezmoi
Vim Script
3
star
27

inject-swaaping-example

A small repository to show swapping out class values to be injected in different modules based on custom providers, useClass, and the inject decorator
TypeScript
3
star
28

nest-super-guard

A small repo to show how two strategies from passport can be used in the same NestJS guard file.
TypeScript
3
star
29

gazer

A simple image upload and viewing server. Nx monorepo with NestJS fastify backend, kysely database connection, and react front end all together
TypeScript
2
star
30

nestjs-timeout-sample

A samll NestJS application that shows how a timeout interceptor can work
TypeScript
2
star
31

NestJS-Playground

A playground repository for me to play around with a lot of different Nest code configurations.
TypeScript
2
star
32

bcrypt-nest-docker

A simple repo to show the use of the docker file from the template and bcrypt, a library that needs bindings from a higher level OS than alpine has
TypeScript
1
star
33

nest-router-fastify-example

Example repo for nest-router working with Fastify
TypeScript
1
star
34

rxjs-timeout-and-retry-demo

A simple repo to work with RxJS and acquiring input on a rolling timeout and resetting the input otherwise
JavaScript
1
star
35

orphansNodeAuth

A Node.js project to test out and use authentication strategies with Passport and Express.
JavaScript
1
star
36

dynamic-metadata-controller-hack

TypeScript
1
star
37

fastify-crf-issue

TypeScript
1
star
38

nest-gql-passport-request-scoped

Sample repo with GQL and Passport-JWT having a request scoped service in the strategy
TypeScript
1
star
39

nest-graphql-union

Small repo to show using GraphQL Unions
TypeScript
1
star
40

pactum-cookie-parsing

JavaScript
1
star
41

kafka-import-error

Minimum Reproduction for Kafka Error I'm facing
TypeScript
1
star
42

NodePug

A Node.js tutorial to understand how to attach Node.js code and .pug files
CSS
1
star
43

nestjs-with-cv-allow

Small reproduction to show Class Validator working with using the @Allow() decorator
TypeScript
1
star
44

nest-dynamic-module-middleware

Showing the configuration of a Dynamic NestJS Module with middleware running successfully.
TypeScript
1
star
45

JUID

A quick Node package to generate a version 4 variant 1 UUID.
JavaScript
1
star
46

mcScribe

Yet another logger for NodeJS
TypeScript
1
star
47

nest-gql-validation-status

A simple repo to show that the HTTP status is still a 200 with the ValidationPipe throwing an error in GraphQL
TypeScript
1
star
48

nestjs-passport-session

Broken repository trying to implement Google OAuth via NestJS and Passport with working sessions. Note: Sessions are broken currently
TypeScript
1
star
49

gql-local-user-deco

TypeScript
1
star
50

fastify-code-failure

Minimum reproduction repository to show `response.code` is not a funciton
TypeScript
1
star
51

nestjs-graphql-guard-middleware

I kinda hate that I wrote this. It's a repository to sho how to use a guard as a middleware so the guard doesn't run multiple times in terms of graphql with NestJS
TypeScript
1
star