开发nodejs cli过程记录

背景

阅读组里常用的 node 编写的 cli 源码记录

npm packages

介绍一些开发 cli 工具很实用的 npm 包

command-exists

node module to check if a command-line command exists

1
2
3
4
5
var commandExists = require('command-exists');
// 异步用法
commandExits('ls', (err, commandExists) => {...});
// 同步用法
commandExists.sync('ls')

fs-extra

fs-extra adds file system methods that aren’t included in the native fs module and adds promise support to the fs methods. It also uses graceful-fs to prevent EMFILE errors. It should be a drop in replacement for fs.
fs-extra 提供了很多 fs 的扩展操作例如复制, 清空等操作, 相当好用
整理一些比较使用的 api:

  1. emptyDir: 字面意思,清空文件夹
  2. readJSON: Reads a JSON file and then parses it into an object.
  3. writeJSON: Write a JSON object stringified into file

ora

Elegant terminal spinner

用于优雅输出终端提示文案
以下是小 demo 及运行输出:

1
2
3
4
5
6
7
const ora = require("ora");
const spinner = ora("default text").start("default text 2");
// spinner.text = 'Loading rainbows';
spinner.info("hello world");
spinner.succeed("hello world");
spinner.fail("hello world");
spinner.succeed();


其中ora需要实例化为spinner使用, ora(), start()参数为string时都表示默认输出文本, spinner.text同理, 后设置的优先级更高, 在spinner.xxx()未传入参数时会输出默认文本

tips

最新版本的ora只能使用 ESModule 引入, commonjs 方式引入请使用 v4 版本

实用操作

查看包的最新版本和所有版本

查看最新版本

1
2
3
npm view package-name versions --json
// or
yarn info package-name versions --json

查看所有版本

1
2
3
4
5
npm view package-name version --json
// "1.0.1205"
// or
yarn info package-name version --json
// {"type":"inspect","data":"1.0.1205"}

process.cwd()和__dirname

这两个命令行的输出在开发 cli 这种全局包中区别尤为明显

process.cwd(): 返回当前工作目录。可以通过process.chdir()更改.
__dirname返回源代码所在的目录。

spawn

我们使用 spawn 执行 linux 命令后如何获得对应的输出呢? 看源码时使用了两种方式, 两种都可以
先看下如何使用 spawn

1
2
const { spawn } = require("child_process");
const ls = spawn("ls", ["-la"], {});
  1. spawn 第三个参数中stdio默认值为 pipe, 相当于相当于 ['pipe', 'pipe', 'pipe'], 对应stdin, stdout, stderr.
    pipe会在子进程和父进程之间创建管道。子进程的标准输入、标准输出和标准错误被重定向到 ChildProcess 对象上相应的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。默认情况下子进程的输出父进程是看不到的, 需要使用以下方式。这里stdio: 'pipe'很容易误解
1
2
3
4
5
6
ls.stdout.setEncoding("utf8");
ls.stdout.pipe(process.stdout);
// or
ls.stdout.on("data", function (data) {
process.stdout.write(data);
});
  1. 设置stdio: 'inherit', 通过相应的标准输入输出流传入/传出父进程, 子进程直接使用父进程的流。 在前三个位置,这分别相当于 process.stdin、process.stdout 和 process.stderr。对于说明性的内容可以直接使用stdio: 'inherit', 对于需要处理的输出数据, 默认即可, 通过stdout.on('data')处理数据
1
2
3
const ls = spawn("ls", ["-la"], { stdio: "inherit" });
// 会直接输出ls -la的内容, 无法通过ondata监听stdout, stderr还是可以监听
// 使用process.stdout无法ondata, 使用ls.stdout.ondata也是会报错的

cwd

spawn 第三个参数中支持 cwd 表示子进程命令要运行的目录, 例如yarn link就需要指定目录才能正常

1
const child = spawn("yarn", ["link"], { cwd: xx });

require.resolve

一般用于获取 npm 包的绝对路径, 如果当前源码运行的目录node_modules不存在这个包, 会直接抛出异常, 用法如下:
博主只用到了模块路径的使用方法, require.resolve会在执行这个函数的目录开始逐级往外查找查找node_modules, 按照这样的搜索顺序["/Users/enhanced-resolve/node_modules", "/Users/node_modules", "/node_modules"]
具体可以参考node 的路径解析 require.resolve
require.resolve和process.cwd()不同, process.chdir()无法更改查找路径

1
2
3
// 获取包所在的目录绝对路径
path.join(require.resolve(${moduleName}/package.json), '..')
// 不加package.json 默认找index.js

reference

  1. nodejs 官方文档 child_process
  2. node 的路径解析 require.resolve

开发nodejs cli过程记录
https://mariana-yui.github.io/2022/06/22/2022-06-22-how-to-write-a-node-cli-tool/
作者
Mariana
发布于
2022年6月22日
许可协议