Node.js命令行设备构建

Node.js命令行工具开发

  • 开发一个类似npm create vite@latestnpx xxx这样的Node.js命令行工具

实例效果展示

  • 模板提供了两个简单的列子
    • 1.创建一个命令行工具模板项目,也就是当前这个项目的源代码
    • 2.从远程拉取创建一个uniapp的模板项目
  • 先全局安装
npm i -g rxm-node-cli
# 安装完成后
# 安装完成后 执行命令行工具,先不指定模板,会通过命令行交互选择模板
rxm-node-cli create project-name
# 或者 
npx rxm-node-cli create project-name
# 指定模板
npx rxm-node-cli create project-name -t cli-template
npx rxm-node-cli create project-name -t uni-template

初始化项目

mkdir rxm-node-cli-tool
cd rxm-node-cli-tool
npm init -y

基本结构

rxm-node-cli-tool/
├── bin/
│   └── cli.js        # 命令行入口文件
├── lib/
│   └── index.js      # 主要逻辑代码
├── package.json
└── README.md

配置package.json

  • bin下的rxm-node-cli就是命令行工具名称
{
"name": "rxm-node-cli",
"version": "1.0.3",
"bin": {
"rxm-node-cli": "./bin/cli.js"
},
"scripts": {
"dev": "node bin/cli.js create",
"upgrade": "standard-version"
},
"standard-version": {
"message": "docs: %s [skip ci]"
},
"dependencies": {
"chalk": "^4.1.0",
"commander": "^9.0.0",
"download-git-repo": "^3.0.2",
"fs-extra": "^11.3.2",
"inquirer": "^8.0.0"
},
"devDependencies": {
"standard-version": "^9.5.0"
}
}

创建启动文件

  • 根目录下创建 bin/cli.js
#!/usr/bin/env node
const { program } = require('commander');
const pkg = require('../package.json');
program
.name('rxm-node-cli')
.description('cli命令行开发模板');
program
.version(pkg.version)
.command('create <project-name>')
  .description('创建一个新项目')
  .option('-t, --template <name>', '指定模板') // 子命令可以配置多个,多次调用option方法即可。得到的option对象会合并
    .action((name,options) => {
    require('../lib/index')({name,template:options.template});
    });
    program.parse(process.argv); // process.argv是启动命令行时传入的参数数组

创建入口文件及其它配置文件

1.lib/index.js 入口文件

// lib/index.js 
const tool = require('./create-cli.js');
const uni = require('./create-uni')
const theme = require('./theme');
const inquirer = require('inquirer');
module.exports = async function ({ name: projectName, template }) {
if (!projectName) return theme.error('请输入项目名称')
if (!template) {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: '请选择模板类型:',
choices: [
{ name: 'cli-template', value: 'cli-template' },
{ name: 'uni-template', value: 'uni-template' }
]
}
]);
template = action
}
if (template === 'cli-template') return tool(projectName)
if (template === 'uni-template') return uni(projectName)
theme.error('模板不存在')
};

2.lib/theme.js 封装打印带样式的文本到终端的函数

// lib/theme.js
const chalk = require('chalk');
module.exports = {
error: text=>{
console.log(chalk.red.bold(text))
},
success: text=>{
console.log(chalk.green(text))
},
successBold: text=>{
console.log(chalk.bold.green(text))
},
warning: text=>{
console.log(chalk.yellow.underline(text))
}
};

3.lib/utlis.js 复用模块封装

const inquirer = require('inquirer');
const fs = require('fs');
const pfs = require('fs-extra');
const theme = require('./theme');
module.exports = {
/**
* 创建前
*/
async beforCreate(projectName) {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: '是否开始创建:',
choices: [
{ name: '是', value: 'yes' },
{ name: '否', value: false }
]
}
]);
if (!action) return
theme.success(`创建项目: ${projectName}`)
// 检查目录是否已存在
if (fs.existsSync(projectName)) {
// 交互式询问用户是否覆盖
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: '目录已存在,请选择:',
choices: [
{ name: '覆盖', value: 'overwrite' },
{ name: '取消', value: false }
]
}
]);
// action等于 choices选择的value
if (!action) return;
if (action === 'overwrite') {
console.log(`\n删除 ${projectName}...`);
// 删除目录
fs.rmSync(projectName, { recursive: true, force: true });
}
}
// 创建项目目录
fs.mkdirSync(projectName);
return true
},
/**
* initJson
*/
initJson(projectName) {
const json = require('../package.json')
json.name = projectName
json.version = '1.0.0'
console.log('创建package.json...')
return pfs.outputFile(
`${projectName}/package.json`,
JSON.stringify(json, null, 2)
);
}
}

4.lib/create-cli.js cli创建逻辑

const path = require('path');
const fs = require('fs');
const pfs = require('fs-extra');
const theme = require('./theme');
const utlis = require('./utlis');
const basePath = path.join(__dirname, '../')
// 拷贝目录
async function copyDirectorySync (src, projectName) {
await pfs.ensureDir(projectName+'/'+src);
const url = path.join(basePath, src)
// 读取源目录内容
const items = fs.readdirSync(url);
for (const item of items) {
pfs.outputFile(
`${projectName}/${src}/${item}`,
fs.readFileSync(path.join(url, item), 'utf-8')
)
}
}
module.exports = async function (projectName) {
await utlis.beforCreate(projectName)
await pfs.ensureDir(projectName);
await utlis.initJson(projectName)
await copyDirectorySync('lib', projectName)
await copyDirectorySync('bin', projectName)
// 拷贝 README.md
pfs.outputFile(
`${projectName}/README.md`,
fs.readFileSync(path.join(basePath, 'README.md'), 'utf-8')
)
theme.success('\n项目创建成功')
theme.successBold(`cd ${projectName}`)
theme.successBold(`npm install`)
}

5.lib/create-uni.js uni项目模板创建

// lib/create-uni.js 
const path = require('path');
const fs = require('fs');
const theme = require('./theme');
const utlis = require('./utlis');
const { promisify } = require('util');
const download = promisify(require('download-git-repo'));
const ora = require('ora');
const chalk = require('chalk');
module.exports = async function (projectName) {
await utlis.beforCreate(projectName);
const spinner = ora('正在创建项目...').start();
try {
spinner.text = '正在下载模板...';
// 确保使用绝对路径
const projectPath = path.resolve(process.cwd(), projectName);
await download('direct:https://gitee.com/ren_jinming/uniapp-vue3-ts-uview-plus.git', projectPath, { clone: true });
console.log('下载完成');
// 修改package.json
spinner.text = '正在修改package.json...';
const packageJsonPath = path.join(projectPath, 'package.json');
// 检查文件是否存在
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`package.json not found at ${packageJsonPath}`);
}
// 读取并修改package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.name = projectName;
// 写入修改后的内容
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
console.log('修改完成');
spinner.succeed(chalk.green('项目创建成功!'));
theme.successBold(`cd ${projectName}`);
theme.successBold(`npm install`);
console.log('使用Hbuild X打开项目')
} catch (error) {
console.error(error);
spinner.warn('项目创建失败');
}
}

命令行工具测试

  • 本地测试
# cli-test作为创建项目的名称
npm run dev cli-test
  • 将命令行工具链接到全局测试
# 安装
npm link
# 如果上面的命令报错可以换一个
npm link --force
# 查看支持哪些命令
rxm-node-cli
# 运行命令行 查询版本
rxm-node-cli -V
# 创建指定的模板
rxm-node-cli create text-project -t cli-template
rxm-node-cli create text-project -t uni-template
# 不指定模板,通过命令行选择
rxm-node-cli create text-project
# 或者用npx 的形式执行
npx rxm-node-cli create text-project

版本管理

  • 安装 standard-version 用于版本管理:
npm install -D standard-version
  • 更新 package.json 配置升级命令及提交msg
{
"scripts": {
"upgrade": "standard-version"
},
"standard-version":{
"message": "docs: %s [skip ci]"
},
}
  • 推荐自动升级方案 会根据 Git 提交记录自动升级版本号,也可以手动设置版本
pnpm upgrade
# 或者
npx standard-version
# 它会读取git的提交信息 并将相关的记录写入到 CHANGELOG.md

发布npm

  • 如果还没有npm账号可以先去注册一个 :https://www.npmjs.com/signup
  • 登录npm
# 执行
npm login
# 在终端输入账号
Username: xxxx (回车)
# 输入密码
Password: xxxx (这里输入密码终端是没有交互反馈的,所以只要确认输入完直接回车)
# 输入邮箱会发送验证码
Email: (this IS public): xxx 回车
# 输入邮箱验证码
Enter one-time password: xxxx 回车
# 登录成功后显示 Logged in as renjinming on https://registry.npmjs.org/.
# 模拟发布
npm pack --dry-run
# 执行后会打印出要上传的信息 如下:
Debugger attached.
npm notice
npm notice   rxm-node-cli@1.0.4
npm notice === Tarball Contents ===
npm notice 503B  CHANGELOG.md
npm notice 596B  README.md
npm notice 622B  bin/cli.js
npm notice 1.1kB lib/create-cli.js
npm notice 1.6kB lib/create-uni.js
npm notice 788B  lib/index.js
npm notice 312B  lib/theme.js
npm notice 1.6kB lib/utlis.js
npm notice 481B  package.json
npm notice === Tarball Details ===
npm notice name:          rxm-node-cli
npm notice version:       1.0.4
npm notice filename:      rxm-node-cli-1.0.4.tgz
npm notice package size:  3.0 kB
npm notice unpacked size: 7.6 kB
npm notice shasum:        2efbb29c6d3731a2abb6333b386f39526b8d1e69
npm notice integrity:     sha512-BrixVR69X/dJd[...]Brt8xuFQWYwnQ==
npm notice total files:   9
npm notice
rxm-node-cli-1.0.4.tgz
Waiting for the debugger to disconnect...
# 实际发布
npm publish
#如果版本号以存在,须要修改版本信息,不然提交会403 执行上面的升级命令在进行发布
➔ 若包名冲突或无权限,会报错。
  • 发布403错误

如果发布403可能是包名已经存在。可以用下面的命令试试看有没有,如果存在可以修改package.json的name重新定义包名

npm view rxm-node-cli   # 检查包名占用

发布完成测试

  • 发布成功验证
# 查询包在npm上的所有版本
npm view rxm-node-cli versions
  • 安装使用
npm i -g rxm-node-cli
# 安装完成后 执行命令行工具
rxm-node-cli create project-name
# 或者 
npx rxm-node-cli create project-name
# 指定模板
npx rxm-node-cli create project-name -t cli-template
npx rxm-node-cli create project-name -t uni-template

各包的用途

commander

  • commander 是 Node.js 最流行的命令行工具开发框架,用于快速构建 CLI(Command Line Interface)应用程序。它可以实现如下功能:
  1. 解析命令行参数(选项、子命令等)
  2. 自动生成帮助信息--help
  3. 提供用户友好的命令行交互体验
核心功能
1. 定义命令和选项
// app.js
const { program } = require('commander');
program
.name('rxm-node-cli')                     // 工具名称
.description('一个示例CLI工具')       // 描述
.version('1.0.0');                   // 版本号
program
.command('create <project-name>')    // 定义子命令
  .description('创建新项目')           // 命令描述
  .option('-t, --template <name>', '指定模板') // 命令选项 // 子命令可以配置多个,多次调用option方法即可。得到的option对象会合并
    .action((name, options) => {         // 执行逻辑
    console.log(`创建项目 ${name},使用模板: ${options.template || '默认'}`);
    });
    program.parse(); // 必须调用
    // 运行 node app.js create my-project -t vue
    // 输出 创建项目 my-project,使用模板: vue
1.自动生成的帮助信息
  • 运行时会自动添加 -h, --help 选项:
node app.js --help
  • 输出示例:
Usage: rxm-node-cli [options] [command]
一个示例CLI工具
Options:
  -V, --version           输出版本号
  -h, --help              显示帮助信息
Commands:
  create    创建新项目
  help [command]          显示命令帮助
核心概念
1. 选项(Options)
program
.option('-d, --debug', '开启调试模式')       // 布尔选项
.option('-p, --port <number>', '端口号', 80) // 带默认值的选项
  .option('-c, --config <path>', '配置文件');  // 必填选项
2. 命令(Commands)
program
.command('add <file>')            // 必需参数
  .description('添加文件')
  .action((file) => {
  console.log(`添加文件: ${file}`);
  });
3. 子命令(Subcommands)
program
.command('server')
.description('服务器操作')
.command('start')                // 子命令
.action(() => {
console.log('启动服务器');
});
实际应用场景
1. 创建脚手架工具(如create-react-app
program
.command('create <app-name>')
  .description('创建新应用')
  .action((name) => {
  // 下载模板、初始化项目等操作
  });
2. 构建开发服务器
program
.command('start')
.option('-p, --port <number>', '端口号', 3000)
  .action((options) => {
  startDevServer(options.port);
  });

chalk

  • chalk 是 Node.js 中最流行的终端字符串样式库,专门用于在命令行工具中输出彩色文本和丰富的格式(如加粗、下划线等)。它能让你的 CLI 工具拥有更美观、更专业的输出效果。
核心功能
1. 改变文字颜色
const chalk = require('chalk');
console.log(chalk.red('错误信息'));      // 红色文字
console.log(chalk.blue('提示信息'));     // 蓝色文字
console.log(chalk.green('成功信息'));    // 绿色文字
2. 改变背景色
console.log(chalk.bgRed.white('白字红底'));
console.log(chalk.bgGreen.black('黑字绿底'));
3. 添加文本样式
console.log(chalk.bold('加粗文本'));
console.log(chalk.underline('下划线文本'));
console.log(chalk.dim('暗淡文本'));
4. 链式调用
console.log(chalk.blue.bgRed.bold('蓝字红底加粗'));
5.256色和RGB支持
console.log(chalk.rgb(255, 136, 0).bold('橙色加粗'))
console.log(chalk.hex('#FF8800')('十六进制颜色'))
实际应用场景
  • 可自定义进行封装,方便调用 ,如下
function showError(message) {
console.log(chalk.red.bold('✖ 错误:'), chalk.red(message));
}
// 输出:✖ 错误: 文件不存在
function showSuccess(message) {
console.log(chalk.green.bold('✓ 成功:'), message);
}
function showWarning(message) {
console.log(chalk.yellow.bold('⚠ 警告:'), message);
}
  • 创建彩色表格,虽然这种方式不太完美但是也勉强能看出是表格了
// 空格
const  space = '    ';
console.log(
chalk.blue('名称') + space +
chalk.green('状态') + space +
chalk.yellow('进度')
);
console.log(
chalk.blue('张三') + space +
chalk.green('已支付') + space +
chalk.yellow('100%')
)
console.log(
chalk.blue('李四') + space +
chalk.green('未支付') + space +
chalk.yellow('0%')
)
高级用法
1.自定义主题函数
const theme = {
error: text=>{
console.log(chalk.red.bold(text))
},
success: text=>{
console.log(chalk.green(text))
},
warning: text=>{
console.log(chalk.yellow.underline(text))
}
};
theme.error('错误信息');
theme.success('成功信息');
theme.warning('警告信息');
2. 组合使用模板字符串
const name = 'Alice';
console.log(chalk`{red 警告:} 用户 {blue.bold ${name}} 不存在`);

ora

  • ora 是 Node.js 中一个专门用于**命令行加载动画(Spinner)**的库,它可以在执行异步操作时显示旋转的加载指示器,为用户提供清晰的执行状态反馈。这个模块特别适合需要等待的操作(如文件下载、安装依赖等)。
核心功能
1. 基本用法
const ora = require('ora');
const spinner = ora('正在加载...').start();
// 模拟异步操作
setTimeout(() => {
spinner.succeed('加载完成');
}, 2000);
2. 状态变化效果
方法效果示例使用场景
.start()⠋ 正在加载…开始加载
.succeed(text)✓ 成功操作成功
.fail(text)✖ 失败操作失败
.warn(text)⚠ 警告出现警告
.info(text)ℹ 信息显示信息
.stop()清除动画手动停止
3.丰富的预设动画:包含超过30种动画效果
spinner.spinner = 'dots'; // 可选:dots, line, pulse等
4.颜色支持:与chalk完美配合
spinner.color = 'yellow'; // 改变颜色
5.速度控制:调整动画刷新率
spinner.interval = 100; // 毫秒(默认80)
6.多行文本支持
spinner.text = '第一行\n第二行';
实际应用场景
1. 文件下载进度
async function downloadFile() {
const spinner = ora('下载文件中...').start();
try {
await download('https://example.com/file.zip');
spinner.succeed('下载完成');
} catch (err) {
spinner.fail('下载失败');
console.error(err);
}
}
2. 安装依赖
async function installDependencies() {
const spinner = ora('正在安装依赖...').start();
try {
await execa('npm', ['install']);
spinner.succeed('依赖安装完成');
} catch {
spinner.fail('依赖安装失败');
}
}
高级用法
1. 自定义动画
const ora = require('ora');
const spinner = ora('正在加载...').start();
spinner.spinner = {
interval: 80,
frames: ['-', '+', '-']
};
// 模拟异步操作
setTimeout(() => {
spinner.succeed('加载完成');
// spinner.fail('加载完成');
}, 3000);
2. 组合进度条
const ora = require('ora');
const spinner = ora({
text: '处理中...',
spinner: {
frames: ['', '', '', ''],
interval: 300
}
}).start();
// 模拟异步操作
setTimeout(() => {
spinner.succeed('加载完成');
// spinner.fail('加载完成');
}, 3000);
3. 动态更新文本
const ora = require('ora');
const spinner = ora({
text: '处理中...',
spinner: {
frames: ['', '', '', ''],
interval: 300
}
}).start();
let i = 1
const time = setInterval(() => {
spinner.text = `已处理 ${i}/${100} 文件`;
i++
}, 1000);
// 模拟异步操作
setTimeout(() => {
spinner.succeed('加载完成');
clearInterval(time);
// spinner.fail('加载完成');
}, 5000);

inquirer

  • 这是一个nodejs命令行交互工具,关于使用可查看我写的另外一篇文章

https://blog.csdn.net/weixin_43376417/article/details/135908186?spm=1011.2415.3001.5331

直接点击目录的 使用inquirer包来实现 查看

  • 整理不易,大家方便的话可帮忙点点赞点点收藏啥的,拜托啦,
posted @ 2025-10-25 14:53  ycfenxi  阅读(2)  评论(0)    收藏  举报