This is the second day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Preliminary knowledge

To develop a scaffold, we usually need to introduce the following libraries:

  • Commander.js: A complete node.js command line solution from TJ
  • Chalk: Embellishes command line output
  • Inquirer. Js: Interrogative response library

If our needs were simple, a scaffold would only need to be implemented using Commander. This article will only use Commander. Js for the demonstration.

The simplest scaffolding

In the project root directory, create bin/cmd.ts as follows:

#! /usr/bin/env node
import { Command } from "commander";

const bootstrap = () = > {
  const program = new Command();
  program
    .version(
      require(".. /package.json").version,
      "-v, --version"."Output the current version."
    )
    .usage("<command> [options]")
    .helpOption("-h, --help"."Output usage information.");
  // Parse the parameters
  program.parse(process.argv);
  // If there is no parameter, output help information
  if(! process.argv.slice(2).length) { program.outputHelp(); }}; bootstrap();Copy the code

The node -r ts-node/register./bin/cmd.ts command output is as follows:

You can see that the console outputs help information, as we expected. But in general, a scaffold should be capable of receiving commands and then performing different actions, as follows:

#! /usr/bin/env node import { Command } from "commander"; const bootstrap = () => { const program = new Command(); program .version( require(".. /package.json").version, "-v, --version", "Output the current version." ) .usage("<command> [options]") .helpOption("-h, --help", "Output usage information.");+ program.command("run").action(() => {
+ console.log("i am running");
+});
+ program.command("sleep").action(() => {
+ console.log("i am sleeping");
+});// Parse the argument program.parse(process.argv); // If there are no arguments, output help information if (! process.argv.slice(2).length) { program.outputHelp(); }}; bootstrap();Copy the code

The above code registers the run and sleep commands and executes the I am running and I am sleeping strings to the console, respectively.

Direction of optimization

At this point, leaving aside the parameters, aliases, options, etc. to register the command, it seems that all we need to do is add commands and actions to the bin/cmd.ts file.

However, writing code is easy, but writing good code is hard. The simple stack code above has the following problems:

  • Files are getting bigger and less readable
  • Not friendly to multiple people developing at the same time, and developing merged code often causes conflicts

At this point, many readers will think that the above two problems can be easily solved by splitting the code, and they are. So how? How do we write scaffolding code that is abstract and scalable?

Command loader

The first thing to know is that program.mand (‘ XXX ‘).action(callback) code needs to be broken up and implemented separately so that there are no conflicts when multiple people are developing different commands at the same time. Therefore, we can develop a command loader like this:

export class CommandLoader {
  public static load(program: Command): void {
    // TODO: register command}}Copy the code

With the command loader, bin/cmd.ts can be fixed to the following code, which is not affected by the addition, deletion, and modification of the command:

#! /usr/bin/env node import { Command } from "commander";+ import { CommandLoader } from "./.. /commands/command.loader";const bootstrap = () => { const program = new Command(); program .version( require(".. /package.json").version, "-v, --version", "Output the current version." ) .usage("<command> [options]") .helpOption("-h, --help", "Output usage information.");- program.command("run").action(() => {
- console.log("i am running");
-});
- program.command("sleep").action(() => {
- console.log("i am sleeping");
-});
+ CommandLoader.load(program);// Parse the argument program.parse(process.argv); // If there are no arguments, output help information if (! process.argv.slice(2).length) { program.outputHelp(); }}; bootstrap();Copy the code

The command abstract

The command pattern is actually fairly rigid: use program to initialize the configuration and call action. So we can define a Command abstract class to improve code reuse and code constraints, and inject action objects into the constructor of the abstract class:

export abstract class AbstractCommand {
  // Dependency injection action
  constructor(protected action: AbstractAction) {}

  public abstract load(program: CommanderStatic): void;
}
Copy the code

Implement a command:

export class RunCommand extends AbstractCommand {
  public load(program: Command): void {
    program.command("run").action(() = > {
      // Pass the parameters and options to the Handle method wrapped as key-value pairs
      this.action.handle(); }); }}Copy the code

The action of abstract

An action accepts parameters and options for logical processing. Both parameters and options are in the form of key-value pairs. The code is as follows:

export interface Input {
  name: string
  value: boolean | string
}

export abstract class AbstractAction {
  public abstracthandle(inputs? : Input[], options? : Input[]):Promise<void>;
}
Copy the code

Implement an action:

export class RunAction extends AbstractAction {
  public asynchandle(inputs? : Input[], options? : Input[]):Promise<void> {
    console.log("i am running"); }}Copy the code

With commands and actions abstracted, registering commands becomes surprisingly simple:

export class CommandLoader {
  public static load(program: Command): void {
    // Add command registration
    new RunCommand(new RunAction()).load(program);
    new SleepCommand(newSleepAction()).load(program); }}Copy the code

conclusion

This paper uses the idea of object-oriented programming to implement a scaffolding framework, with the following advantages:

  • Executable files (here isbin/cmd.ts) and the command
  • To implement a command loader, it takes only one line of code to register a command
  • Abstract Commands and actions to improve code reuse and code constraints
  • Action dependencies are injected into commands to improve scalability

Due to limited space, this article has omitted many details of command and Action configuration and handling. If you have any questions, you can discuss them in the comments section

All of the code for this article is available on Github

Refer to the link

  • nest-cli