Skip to content

⚠️ 免责声明: 本文档由 AI 自动生成,仅供参考学习使用。

学习前置准备

在学习本课程之前,请先将 nest-commander 仓库克隆到本地 external 目录:

# 使用 depth=1 减少克隆时间
git clone --depth=1 https://github.com/jmcdo29/nest-commander.git docs/research/external/nest-commander

当前文档信息:

  • 研究分支 (Branch): master
  • Git SHA1: b0493c16e52636fc05edf6dcb7d42c573bbbd0a3
  • 外部仓库位置: docs/research/external/nest-commander/

Nest Commander 学习资料

1. 项目概述

Nest Commander 是一个为 NestJS 框架设计的 CLI (命令行界面) 构建工具。它允许开发者使用与 NestJS 相同的装饰器模式和依赖注入机制来构建命令行应用程序。该项目基于流行的 Commander.js 包构建。

  • GitHub 仓库: https://github.com/jmcdo29/nest-commander
  • 当前版本: 3.20.1
  • 许可证: MIT
  • 作者: Jay McDoniel

2. 项目架构

2.1 Monorepo 结构

该项目使用 Nx 作为 Monorepo 管理工具,包含以下主要包:

包名 版本 描述
nest-commander 3.20.1 核心包,提供 CLI 构建能力
nest-commander-testing 3.5.1 测试工具包
nest-commander-schematics 3.2.0 Angular Schematics 代码生成工具

2.2 目录结构

nest-commander/
├── apps/
│   └── docs/              # Astro 文档站点
├── packages/
│   ├── nest-commander/    # 核心包
│   ├── nest-commander-testing/  # 测试工具
│   └── nest-commander-schematics/ # 代码生成
├── integration/           # 集成测试 (21 个测试用例)
├── package.json           # 根 package.json
└── nx.json               # Nx 配置

2.3 核心依赖

主要依赖:

  • commander: 11.1.0 - CLI 参数解析
  • @golevelup/nestjs-discovery: 5.0.0 - NestJS 服务发现
  • inquirer: 8.2.7 - 交互式命令行提示
  • cosmiconfig: 8.3.6 - 配置文件加载
  • @fig/complete-commander: 3.0.0 - 命令补全

对等依赖 (Peer Dependencies):

  • @nestjs/common: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
  • @nestjs/core: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0

3. 核心功能模块

3.1 命令定义 (@Command() 装饰器)

使用装饰器定义 CLI 命令:

@Command({
  name: 'greet',
  arguments: '<name>',
  description: 'Greet someone',
})
export class GreetCommand implements CommandRunner {
  async run([name]: string[], options: Record<string, any>): Promise<void> {
    console.log(`Hello, ${name}!`);
  }
}

3.2 选项定义 (@Option() 装饰器)

@Option({
  flags: '-l, --love',
  description: 'Express your love',
  defaultValue: false,
})
parseLove(val: string): boolean {
  return JSON.parse(val);
}

3.3 命令工厂 (CommandFactory)

import { CommandFactory } from 'nest-commander';

async function bootstrap() {
  await CommandFactory.run(RootModule, ['greet', 'bye']);
}

4. 集成测试覆盖

项目包含 21 个集成测试用例,覆盖以下功能场景:

测试目录 功能描述
basic 基础命令功能
multiple 多命令支持
sub-commands 子命令
default-sub-commands 默认子命令
root-command 根命令
this-command 命令嵌套
this-handler 自定义处理器
dot-command 点命令
help-tests 帮助信息
version-option 版本选项
option-choices 选项可选值
output-config 输出配置
pizza Inquirer 交互式提问
with-questions 交互式问题
plugins 插件系统
register-provider 自定义 Provider
request-provider-override Provider 覆盖

5. 学习计划

5.1 入门阶段 (预计 2-3 小时)

  1. 环境准备

  2. 安装 Node.js (推荐 LTS 版本)

  3. 克隆项目仓库
  4. 安装 pnpm 依赖: pnpm install

  5. 基础概念学习

  6. 阅读官方文档: https://nest-commander.jaymcdoniel.dev

  7. 理解 @Command() 装饰器
  8. 理解 @Option() 装饰器
  9. 了解 CommandRunner 接口

  10. Hello World 示例

  11. 创建第一个命令

  12. 使用 CommandFactory 启动

5.2 进阶阶段 (预计 3-4 小时)

  1. 依赖注入

  2. 在命令中使用 NestJS 依赖注入

  3. 自定义 Provider 注册

  4. 子命令与嵌套

#### 5.1 子命令基础用法

使用 @SubCommand() 装饰器定义子命令,通过父命令的 subCommands 数组注册:

import { Command, CommandRunner } from 'nest-commander';
import { SubCommand } from 'nest-commander';

// 定义子命令
@SubCommand({ name: 'mid-1' })
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('mid-1 command executed');
  }
}

// 父命令注册子命令
@Command({
  name: 'top',
  subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
  async run() {
    console.log('top command');
  }
}

#### 5.2 子命令嵌套

子命令可以拥有自己的子命令,形成多层嵌套结构:

// 底层子命令
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
  async run() {
    console.log('bottom command');
  }
}

// 中层子命令(拥有自己的子命令)
@SubCommand({ name: 'mid-1', subCommands: [BottomCommand] })
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('mid-1 command');
  }
}

// 顶层命令
@Command({
  name: 'top',
  subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
  async run() {
    console.log('top command');
  }
}

运行效果:

  • top → 执行 TopCommand
  • top mid-1 → 执行 Mid1Command
  • top mid-1 bottom → 执行 BottomCommand

#### 5.3 默认子命令

使用 options: { isDefault: true } 设置默认子命令,当不指定子命令时自动执行:

@SubCommand({
  name: 'mid-1',
  subCommands: [BottomCommand],
  options: { isDefault: true },  // 设置为默认子命令
})
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('default sub command');
  }
}

运行 top 时会自动执行 mid-1 子命令。

#### 5.4 子命令别名

使用 aliases 为子命令设置简短别名:

@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
  async run() {
    console.log('mid-2 command');
  }
}

现在可以使用 top mid-2top m 两种方式调用。

#### 5.5 完整示例:多层嵌套命令

结合以上特性,创建一个完整的多层嵌套命令:

// bottom.command.ts
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('bottom command executed'); }
}

// mid-1.command.ts(带默认子命令)
@SubCommand({
  name: 'mid-1',
  subCommands: [BottomCommand],
  options: { isDefault: true },
})
export class Mid1Command extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('mid-1 command executed'); }
}

// mid-2.command.ts(带别名)
@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('mid-2 command executed'); }
}

// top.command.ts
@Command({
  name: 'top',
  subCommands: [Mid1Command, Mid2Command],
})
export class TopCommand extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run(inputs: string[]) { this.log.log('top command'); }
}

命令调用对照表:

命令 执行结果
top Mid1Command(默认)→ BottomCommand(默认)
top mid-1 Mid1Command
top mid-1 bottom BottomCommand
top mid-2 Mid2Command
top m Mid2Command(别名)
  1. 选项处理

  2. 必选选项 vs 可选选项

  3. 选项类型转换 (boolean, number, array)

5.3 高级阶段 (预计 4-5 小时)

  1. 交互式命令行

#### 7.1 基础用法 - InquirerService 注入

通过注入 InquirerService 实现交互式命令行:

import { Command, CommandRunner, InquirerService } from 'nest-commander';

@Command({ name: 'hello' })
export class HelloCommand extends CommandRunner {
  constructor(private readonly inquirer: InquirerService) {
    super();
  }

  async run(_inputs: string[], options?: HelloOptions): Promise<void> {
    // 询问问题集 'hello',传入已解析的选项
    options = await this.inquirer.ask('hello', options);
    console.log(`Hello ${options.name}`);
  }
}

#### 7.2 问题定义 - @QuestionSet 和 @Question 装饰器

使用装饰器定义问题集:

import { Question, QuestionSet } from 'nest-commander';

@QuestionSet({ name: 'hello' })
export class WhoQuestion {
  @Question({
    message: 'What is your name?',
    name: 'name',
  })
  parseName(val: string) {
    return val;
  }
}

常用问题类型

类型 描述 适用场景
input 文本输入 姓名、地址等
confirm 是/否确认 布尔值选项
list 列表选择 单选
rawlist 数字索引列表 单选(带编号)
expand 展开列表 单选(带快捷键)
checkbox 复选框 多选
editor 编辑器 长文本输入

#### 7.3 问题验证 - @ValidateFor 装饰器

使用 @ValidateFor 添加字段验证:

@ValidateFor({ name: 'phone' })
validatePhone(value: string) {
  const pass = value.match(/^\d{10}$/);
  if (pass) {
    return true;
  }
  return 'Please enter a valid 10-digit phone number';
}

#### 7.4 条件问题 - @WhenFor 装饰器

根据前一个答案决定是否显示某个问题:

@WhenFor({ name: 'prize' })
whenPrize(answers: { comments: string }): boolean {
  // 只有当 comments 不是默认值时才显示 prize 问题
  return answers.comments !== 'Nope, all good!';
}

#### 7.5 完整示例:Pizza 订单

综合运用所有特性:

// pizza.interface.ts
export interface PizzaOptions {
  toppings?: string;
  toBeDelivered?: boolean;
  phone?: string;
  size?: string;
  quantity?: number;
  beverage?: string;
  comments?: string;
  prize?: string;
}

// pizza.question.ts
import { Question, QuestionSet, ValidateFor, WhenFor } from 'nest-commander';

@QuestionSet({ name: 'pizza' })
export class PizzaQuestion {
  // 展开列表选择
  @Question({
    type: 'expand',
    name: 'toppings',
    message: 'What about the toppings?',
    choices: [
      { key: 'p', name: 'Pepperoni and cheese', value: 'PepperoniCheese' },
      { key: 'a', name: 'All dressed', value: 'alldressed' },
      { key: 'w', name: 'Hawaiian', value: 'hawaiian' },
    ],
  })
  parseToppings(val: string) { return val; }

  // 确认问题
  @Question({
    type: 'confirm',
    name: 'toBeDelivered',
    message: 'Is this for delivery?',
    default: false,
  })
  parseToBeConfirmed(val: boolean) { return val; }

  // 带验证的输入
  @Question({
    type: 'input',
    name: 'phone',
    message: "What's your phone number?",
  })
  parsePhone(val: string) { return val; }

  @ValidateFor({ name: 'phone' })
  validatePhone(value: string) {
    const pass = value.match(/^\d{10}$/);
    return pass ? true : 'Please enter a valid phone number';
  }

  // 列表选择
  @Question({
    type: 'list',
    name: 'size',
    message: 'What size do you need?',
    choices: ['Large', 'Medium', 'Small'],
  })
  parseSize(val: string) { return val.toLowerCase(); }

  // 条件问题
  @WhenFor({ name: 'prize' })
  whenPrize(answers: { comments: string }): boolean {
    return answers.comments !== 'Nope, all good!';
  }
}

// pizza.command.ts
import { Command, CommandRunner, InquirerService } from 'nest-commander';
import { PizzaOptions } from './pizza.interface';

@Command({ name: 'pizza', options: { isDefault: true } })
export class PizzaCommand extends CommandRunner {
  constructor(private readonly inquirerService: InquirerService) {
    super();
  }

  async run(_inputs: string[], options?: PizzaOptions): Promise<void> {
    options = await this.inquirerService.ask('pizza', options);
    console.log(options);  // 输出完整订单
  }
}

执行效果

$ my-cli pizza
? What about the toppings? (p) Pepperoni and cheese, (a) All dressed, (w) Hawaiian
? Is this for delivery? No
? What's your phone number? 1234567890
? What size do you need? Large
? How many do you need? 2
? You also get a free 2L beverage Pepsi
? Any comments on your purchase experience? Great service!
? For leaving a comment, you get a freebie cake
{ toppings: 'PepperoniCheese', toBeDelivered: false, phone: '1234567890', ... }

#### 7.6 在模块中注册问题

确保在 NestJS 模块中注册问题和命令:

import { Module } from '@nestjs/common';
import { PizzaCommand } from './pizza.command';
import { PizzaQuestion } from './pizza.question';

@Module({
  providers: [PizzaCommand, PizzaQuestion],
})
export class PizzaModule {}
  1. 配置管理

#### 8.1 cosmiconfig 集成

Nest Commander 内置支持 cosmiconfig,用于自动搜索和加载配置文件。CommandFactory.run() 会自动调用 cosmiconfig 搜索配置文件。

支持的配置文件格式

文件名 格式
.myclirc JavaScript
.myclirc.json JSON
.myclirc.yaml YAML
.myclirc.yml YAML
myclirc.json JSON
myclirc.yaml YAML
myclirc.yml YAML
package.json 包含 mycli 字段

(假设 CLI 名称为 mycli

#### 8.2 配置文件位置搜索顺序

cosmiconfig 从项目根目录开始搜索:

  1. 当前目录
  2. 逐级向上查找直到根目录

#### 8.3 插件配置

在配置文件中使用 plugins 字段加载自定义插件:

// .myclirc.json
{
  "plugins": ["./plugins/my-plugin"]
}
// my-plugin.js - 导出默认 NestJS 模块
import { Module } from '@nestjs/common';

@Module({
  providers: [MyCustomProvider],
})
export class MyPluginModule {}

工作原理

  1. CommandFactory.run() 调用 cosmiconfig 搜索配置文件
  2. 读取配置中的 plugins 数组
  3. 使用 import() 动态加载插件模块
  4. 将插件模块添加到 NestJS 应用上下文中

#### 8.4 环境变量处理

@Option 装饰器中使用 env 属性绑定环境变量:

import { Command, CommandRunner, Option } from 'nest-commander';

@Command({ name: 'config' })
export class ConfigCommand extends CommandRunner {
  async run() {
    console.log('Config loaded from environment');
  }

  @Option({
    flags: '-u, --username <username>',
    description: 'Username',
    env: 'MYCLI_USERNAME',  // 绑定环境变量
  })
  parseUsername(val: string): string {
    return val;
  }

  @Option({
    flags: '-t, --token <token>',
    description: 'API Token',
    env: 'MYCLI_TOKEN',  // 绑定环境变量
  })
  parseToken(val: string): string {
    return val;
  }
}

优先级:命令行参数 > 环境变量 > 默认值

#### 8.5 完整配置示例

假设 CLI 名称为 myapp

// .myapprc.json
{
  "plugins": [
    "./plugins/auth-plugin",
    "./plugins/logger-plugin"
  ],
  "verbose": true,
  "logLevel": "debug"
}
// bootstrap.ts
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

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

bootstrap();

执行时,Nest Commander 会自动:

  1. 搜索 .myapprc.json 配置文件
  2. 加载 auth-pluginlogger-plugin 模块
  3. 将它们注册到 NestJS 应用中

#### 8.6 自定义配置加载位置

如果需要自定义配置加载行为,可以扩展 CommandFactory

import { CommandFactory } from 'nest-commander';

class CustomCommandFactory extends CommandFactory {
  // 自定义逻辑
}

但通常不需要这样做,默认的 cosmiconfig 集成已足够满足大多数场景。

  1. 测试

  2. 使用 nest-commander-testing 进行单元测试

  3. 集成测试编写

5.4 生产实践 (预计 2-3 小时)

  1. 插件系统

    • 理解 Commander 插件机制
    • 自定义插件开发
  2. Schematics

    • 使用 nest-commander-schematics 生成代码
    • 自定义 schematics 开发
  3. 最佳实践

    • 错误处理
    • 日志记录
    • 构建与发布

6. 关键文件索引

文件路径 描述
packages/nest-commander/src/command.decorators.ts @Command() 和 @Option() 装饰器实现
packages/nest-commander/src/command.factory.ts CommandFactory 核心类
packages/nest-commander/src/command-runner.service.ts 命令执行服务
packages/nest-commander/src/inquirer.service.ts Inquirer 集成服务
packages/nest-commander-testing/src/command-test.factory.ts 测试工具工厂
integration/basic/src/basic.command.ts 基础命令示例

7. 相关资源

  • 官方文档: https://nest-commander.jaymcdoniel.dev
  • GitHub Issues: https://github.com/jmcdo29/nest-commander/issues
  • Commander.js 文档: https://github.com/tj/commander.js
  • NestJS 文档: https://docs.nestjs.com