使用现代的 Node.js 构建简单的CLI工具

88 阅读7分钟

介绍了如何使用现代的 Node.js 构建简单的 CLI 工具,只使用 Node.js 的标准库,不安装外部依赖,通过示例展示了工具的功能和原理。作者分享了采用 ESM、JSDoc 注释、Node 内置的参数解析和测试框架等技术,展示了构建 CLI 工具的最佳实践和工作流程。最后,提供了带注释的 package.json 示例,展示了如何配置一个 CLI 工具的 npm 包,整体概括了构建 CLI 工具的过程和注意事项。今日前端早读课文章由 @粥里有勺糖翻译分享,公号:粥里有勺糖授权。

正文从这开始~~

只使用 Node.js 的标准库,不安装任何外部依赖,写一个命令行工具。

作者是多个开源项目的维护者,长久以来都是手动维护项目的变更日志(changelog)。

下面是项目 a12n-server 的变更日志示例:github.com/curveball/a…

 0.22.0 (2022-09-27)
 -------------------

 Warning note for upgraders. This release has a database migration on the
 `oauth2_tokens` table. For most users this is the largest table, some
 downtime may be expected while the server runs its migrations.

 * #425: Using a `client_secret` is now supported with `authorization_code`,
   and it's read from either the request body or HTTP Basic Authorization
   header.
 * The service now keeps track when issuing access tokens, whether those tokens
   have used a `client_secret` or not, which `grant_type` was used to issue them
   and what scopes were requested. This work is done to better support OAuth2
   scopes in the future, and eventually OpenID Connect.
 * Fixed broken 'principal uri' in introspection endpoint response.
 * OAuth2 service is almost entirely rewritten.
 * The number of tokens issued is now displayed on the home page.
 * Large numbers are now abbreviated with `K` and `M`.
 * #426: Updated to Curveball 0.20.
 * #427: Typescript types for the database schema are now auto-generated with
   `mysql-types-generator`.

内容使用 markdown 编写的。你可能会想:Git 不是有 commit 日志吗?为什么要费力手写这个呢?

原因是它们的受众不一样。我希望让用户关注到相对重要的变更事项,并同时注意到变化对用户带来的影响。

我觉得写一个命令行工具来做这些事会更方便,维护多个项目(如此多的变更)也更容易一些。所以,我就做了 changelog-tool!如果你想知道这背后涉及了哪些技术选择,请接着往下阅读。

目标 & 特色

工具已支持的功能:

  • 重新格式化日志(有点像美化)( changelog format );
  • 通过命令行添加一行日志( changelog add --minor -m "New feature" );
  • 自动设置发布日期( changelog release );
  • 将特定版本的日志导入标准输出,以便其他工具可以使用(例如与 GitHub 发布集成)。

我还有一些非功能性的需求:

  • 使用最新的 Node.js 特性;
  • 使用最新的 JavaScript 标准和特性(ESM);
  • 避免非必要的外部依赖;
  • 低维护成本。

想立即找到这个工具吗?它是开源的,你只需访问 Github:github.com/evert/chang…

原理解析

ESM & Typescript ESM

ESM 模块现在使用起来已经非常丝滑了。这是习惯上的一个小改变,但我一般建议是将文件保存为 .mjs 来使用 ESM。

下面是 parse.mjs 的前几行代码:

 import { readFile } from 'node:fs/promises'
 import { Changelog, VersionLog } from './changelog.mjs'

 /**
  * @param {string} filename
  * @returns {Promise<Changelog>}
  */
 export async function parseFile(filename) {
   return parse(
     await readFile(filename, 'utf-8')
   )
 }

CommonJS -> ESM 的过渡并非没有痛苦,但对于像这样的新项目来说,它是非常理想的选择。(顶层 await 🎉)

我还选择不使用 Typescript 编写代码,取而代之的是使用 JSDoc 注释(上面是 @param 和 @returns 注释)。

当然不是每个人都知道不写 .ts 文件也可获得和 Typescript 一样的体验。Typescript 也可以严格地检查 Javascript 文件。

这样做的好处是项目不需要构建步骤了。甚至在开发过程中不需要用到 Typescript,降低了上手门槛。

这是我的简化后的 tsconfig.json 配置文件:

 {
   "compilerOptions": {
     "target": "es2022",
     "module": "esnext",
     "rootDir": "./",
     "allowJs": true,
     "checkJs": true,

     "moduleResolution": "node",

     "noEmit": true,
     "strict": true,
     "useUnknownInCatchVariables": false
   }
 }

如果您想了解更多信息,Typescript 文档有一个页面详细介绍了支持的 JSDoc 注释 [5]。

命令行参数解析

CLI 工具需要支持解析命令行选项参数 (option)。从 Node 18.3(向后移植到了 Node 16.17)开始,Node 内置了一个参数解析的方法。

下面是代码示例:

 import { parseArgs } from 'node:util'

 const { positionals, values } = parseArgs({
   options: {
     help: {
       type: 'boolean',
       short: 'h',
       default: false,
     },
     all: {
       type: 'boolean',
       default: false,
     },
     message: {
       type: 'string',
       short: 'm'
     },
     patch: { type: 'boolean' },
     minor: { type: 'boolean' },
     major: { type: 'boolean' },
   },
   allowPositionals: true,
 })

这段配置代码添加了诸如 --major 这些参数的解析支持,这样就可以使用 --message "hello!" 指定消息,也可以使用 -m "Hi" 简写的方式替代。

它能完成所有工作吗?不!在一些复杂的工具中,还会用到彩色打印,自动创建帮助信息等,但这样就需要引入许多额外的第三方依赖。

就我而言,已经够用了。

可以查看 Node.js 对应文档了解更详细的内容。

测试

大多数人可能会使用 Jest 或 Mocha 作为测试框架,但从 Node 18(也向后移植到 16)开始,Node 内置了运行测试用例的能力。

它的 API 类似于 Mocha 和 Jest ,包含 it 、 test 、 describe 、 before 等关键字。

下面是一个单侧的示例:

 // @ts-check
 import * as assert from 'node:assert'
 import { test } from 'node:test'
 import { parse } from '../parse.mjs'

 test('Parsing changelog metadata', async () => {
   const input = `Time for a change
 =========

 0.2.0 (????-??-??)
 ------------------

 * Implemented the 'list' command.
 * Added testing framework.

 0.1.0 (2023-02-08)
 ------------------

 * Implemented the 'help' and 'init' commands.
 *
 `

   const result = await parse(input)

   assert.equal('Time for a change', result.title)
   assert.equal(2, result.versions.length)

   assert.equal(null, result.versions[0].date)
   assert.equal('0.2.0', result.versions[0].version)
   assert.equal('2023-02-08', result.versions[1].date)
   assert.equal('0.1.0', result.versions[1].version)
 })

要执行测试,只需运行 node --test 指令即可。无需额外配置,它将自动扫描遵循规范的目录和单测文件。

Node 18 测试输出有点简陋,它是 TAP 格式,如下所示:

 TAP version 13
 # Subtest: /home/evert/src/changelog-tool/test/parse.mjs
     # Subtest: Parsing changelog metadata
     ok 1 - Parsing changelog metadata
       ---
       duration_ms: 1.713409
       ...
     # Subtest: Parsing changelog entries
     ok 2 - Parsing changelog entries
       ---
       duration_ms: 0.2595
       ...
     # Subtest: Preface and postface
     ok 3 - Preface and postface
       ---
       duration_ms: 0.193591
       ...
     1..3
 ok 1 - /home/evert/src/changelog-tool/test/parse.mjs
   ---
   duration_ms: 70.901055
   ...
 1..1
 # tests 1
 # pass 1
 # fail 0
 # cancelled 0
 # skipped 0
 # todo 0
 # duration_ms 81.481441

坦白说,用了这个之后我不确定是否还会继续再使用 Mocha。毕竟我已经用了十多年了,还是有许多不错的功能,我认为除了 Node 已经支持的外 Mocha 里还有一些我需要的功能。

一些链接:

带注释的 package.json

我想以我配置的 package.json 来结束这篇文章,这样你就可以看到前面内容是如何联系到一起的。(如果 npm 支持 JSON5,这样我就可以将我的注释保留在包中 😭)。

 {
   // 包的名称
   "name": "changelog-tool",

   // 包的版本号
   "version": "0.5.0",

   // 这将显示在NPM搜索结果中
   "description": "A CLI tool for manipulating changelogs",

   // 这告诉Node这是一个ESM包
   // 当然不是严格需要的,如果我们在每个地方都是使用 .mjs
   "type": "module",

   // 如果需要在编码的时候使用此包中的方法(不是 CLI 中),则需要在这里指定导出的模块入口文件
   "main": "index.mjs",

   "scripts": {
     // 运行测试用例
     "test": "node --test",

     // 我喜欢让 Typescript 在终端中运行,以便有任何问题即时的警告我
     "watch": "tsc --watch"
   },

   // 方便更好的在 npmjs.org 上发现此包
   "keywords": [
     "changelog",
     "markdown"
   ],

   // 作者信息
   "author": "Evert Pot (https://evertpot.com/)",

   // 做任何你想做的事(MIT协议基本没有约束)
   "license": "MIT",

   "engine": {
     // 警告尚未升级的用户
     "node": ">16"
   },

   "bin": {
     // 当人们安装这个包时,可以通过 `npx changelog` 执行。
     // 如果全局安装了这个包,就会有一个 `changelog` 命令
     "changelog": "./cli.mjs"
   },
   "devDependencies": {
     // 唯一的 2 个依赖项。如果你想?甚至不需要这些包
     "@types/node": "^18.11.19",
     "typescript": "^4.9.5"
   }
 }

总结

我喜欢创造新事物并深思熟虑地做出每一个决定。

结果是我更有可能最终得到一些简约、维护成本低的东西,并且让我对所使用的工具有更深入的理解。

未来我可能会再次做出这些选择。Node 的测试工具快速又简单,ESM 也非常 Nice,还不需要引入构建,对于我这种规模的项目来说,感觉非常合适。

我希望这能鼓励将来的人从空目录来开始他们的下一个项目,而不是复制大型项目模板。

Github 上的 changelog-tool 项目:github.com/evert/chang…

最后一句话确实挺值得思考的,现在好像无论做什么项目,都想先找个模板然后才开始🤔