⚠️ 免责声明: 本文档由 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 小时)
-
环境准备
-
安装 Node.js (推荐 LTS 版本)
- 克隆项目仓库
-
安装 pnpm 依赖:
pnpm install -
基础概念学习
-
阅读官方文档: https://nest-commander.jaymcdoniel.dev
- 理解
@Command()装饰器 - 理解
@Option()装饰器 -
了解
CommandRunner接口 -
Hello World 示例
-
创建第一个命令
- 使用
CommandFactory启动
5.2 进阶阶段 (预计 3-4 小时)
-
依赖注入
-
在命令中使用 NestJS 依赖注入
-
自定义 Provider 注册
-
子命令与嵌套
#### 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→ 执行 TopCommandtop mid-1→ 执行 Mid1Commandtop 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-2 或 top 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(别名) |
-
选项处理
-
必选选项 vs 可选选项
- 选项类型转换 (boolean, number, array)
5.3 高级阶段 (预计 4-5 小时)
- 交互式命令行
#### 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 {}
- 配置管理
#### 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 从项目根目录开始搜索:
- 当前目录
- 逐级向上查找直到根目录
#### 8.3 插件配置
在配置文件中使用 plugins 字段加载自定义插件:
// my-plugin.js - 导出默认 NestJS 模块
import { Module } from '@nestjs/common';
@Module({
providers: [MyCustomProvider],
})
export class MyPluginModule {}
工作原理:
CommandFactory.run()调用cosmiconfig搜索配置文件- 读取配置中的
plugins数组 - 使用
import()动态加载插件模块 - 将插件模块添加到 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 会自动:
- 搜索
.myapprc.json配置文件 - 加载
auth-plugin和logger-plugin模块 - 将它们注册到 NestJS 应用中
#### 8.6 自定义配置加载位置
如果需要自定义配置加载行为,可以扩展 CommandFactory:
import { CommandFactory } from 'nest-commander';
class CustomCommandFactory extends CommandFactory {
// 自定义逻辑
}
但通常不需要这样做,默认的 cosmiconfig 集成已足够满足大多数场景。
-
测试
-
使用
nest-commander-testing进行单元测试 - 集成测试编写
5.4 生产实践 (预计 2-3 小时)
-
插件系统
- 理解 Commander 插件机制
- 自定义插件开发
-
Schematics
- 使用
nest-commander-schematics生成代码 - 自定义 schematics 开发
- 使用
-
最佳实践
- 错误处理
- 日志记录
- 构建与发布
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