# 创建 Node.js 命令行工具
最近又接到一个需求:汉化一个英文 Angular 项目。Angular 不是使用 JSX/TSX 来构建 UI 的,而是使用 HTML 模版来构建的,所以不能使用我上一篇文章提到的 jscodeshift
工具。这次我使用了 posthtml
(opens new window)(cheerio
(opens new window) 也是一个不错的选项),并且我打算创建一个 Node.js 命令行工具 chinesize,它将使用 posthtml
(opens new window) 来汉化 Angular 项目。
# 汉化 Angular 项目
汉化 Angular 项目和上一篇汉化 React 项目的整体思路是一样的,提取代码里的英文文本 -> 翻译英文文本 -> 替换代码里的英文文本,只是操作对象不一样,汉化 React 项目需要解析 JSX/TSX 文件,而汉化 Angular 项目需要解析 HTML 文件以及 Angular 模版(其本质还是 HTML),所以汉化 React 项目使用 jscodeshift
工具,而汉化 Angular 项目使用 posthtml
工具。
汉化 Angular 项目分两种情况:
HTML 文件,直接使用
posthtml
工具进行汉化Angular 模版,需要提取模版字符串,然后汉化模版字符串,最后将汉化后的模版字符串替换原来的模版字符串,比如
@Component({
selector: 'metrics-pinned-view-component',
template: `
<div class="group-toolbar">
<div
class="right-items"
*ngIf="cardIdsWithMetadata.length > 0 && globalPinsEnabled"
>
<button
mat-stroked-button
aria-label="Clear all pinned cards"
(click)="onClearAllPinsClicked.emit()"
>
Clear all pins
</button>
</div>
</div>
<ng-template #emptyPinnedView>
<div class="empty-message">Pin cards for a quick view and comparison</div>
</ng-template>
`
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# posthtml
posthtml
(opens new window) 是一个转换 HTML/XML 的工具,主要由两部分组成:
posthtml-parser
(opens new window): 一个 HTML 解析器,解析 HTML/XML 到 HTML 节点树posthtml-render
(opens new window): 一个 HTML 渲染器,渲染 HTML 节点树到 HTML/XML
所有的 HTML 转换都是通过 posthtml
插件来完成的。这些插件只是简单的 JS 函数,它们接收 HTML节点树,对其进行转换,并返回修改后的树。我们可以使用 posthtml
提供的插件 (opens new window),也可以自己定义插件。
// myPlugin.js
export default function myPlugin() {
return (tree) => {
// tree 是 HTML Node Tree
// 通过 tree 去遍历所有的树节点
tree.match({ tag: 'img' }, (node) => {
// 遍历所有的 <img> 元素
// 可以修改 <img> 元素
})
}
}
2
3
4
5
6
7
8
9
10
11
HTML Node Tree (opens new window) 提供了两个 API 来遍历树节点
walk(cb)
:遍历树中的所有节点match(expression, cb)
:匹配表达式,搜索对应的树节点
然后就可以使用自定义的插件转换 HTML/XML 了
import myPlugin from "./myPlugin.js"
import posthtml from "posthtml";
const html = `
<html>
<body>
<p class="wow">OMG</p>
</body>
</html>
`;
posthtml([myPlugin()])
.process(html /*, options */)
.then(result => console.log(result.html));
2
3
4
5
6
7
8
9
10
11
12
13
14
process
默认使用 posthtml-parser
解析 HTML/XML,生成 HTML 节点树,然后通过插件遍历或修改 HTML 树节点,最后通过 posthtml-render
转换回 HTML/XML。
process
函数支持这些选项参数 (opens new window),如果你使用默认的 posthtml-parser
解析器,它也支持 posthtml-parser
(opens new window) 的选项 (opens new window)。
# 创建 chinesize 项目
言归正传,接下来我们将创建 node.js 命令行工具:chinesize,它将使用 posthtml
来汉化我们的 Angular 项目。
首先我们创建 chinesize 项目
也可以使用 yarn 或者 pnpm
$ npm init
它会引导你一步一步创建 node.js 项目,你也可以添加 --yes
参数,直接使用默认值创建,更多详情请参考 npm init (opens new window)。
最后会在当前目录下创建一个 package.json
文件
{
"name": "chinesize",
"version": "1.0.0",
"description": "Convert English React/Angular project to Chinese",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "cp3hnu",
"license": "ISC"
}
2
3
4
5
6
7
8
9
10
11
12
Node.js 支持两种模块规范: CommonJS(CJS)和 ECMAScript(ESM),默认是 CommonJS,可以通过在 package.json
文件里通过 type
改为 ESM。
{
type: module
}
2
3
CJS 和 ESM 的区别,可以参考 What the heck are CJS, AMD, UMD, and ESM in Javascript? (opens new window)
# 添加命令
接下来我们添加 chinesize
命令
首先在项目里创建 bin
文件夹,然后创建 chinesize.js
文件(文件名无所谓,这里只是为了统一)。为了让 chinesize.js
能在命令行工具里运行,我们需要在文件开头添加 shebang (opens new window)
#!/usr/bin/env node
同时还需要添加执行权限
$ chomd 755 bin/chinesize.js
最后在 package.json
里添加 bin
属性
{
"bin": "bin/chinesize.js"
}
2
3
如果支持多个命令需要使用对象
{
"bin": {
"chinesize": "bin/chinesize.js"
}
}
2
3
4
5
完成上面的操作之后,如果全局安装我们的包,在 Mac 电脑上 bin/chinesize.js
文件将会被链接到 /usr/local/bin/chinesize
,这个路径已经添加到我们的 $PATH
环境变量里,所以就可以执行命令 chinesize
。在 Windows 电脑将创建一个 cmd 命令文件来执行bin/chinesize.js
文件
# npm link
在发布之前,我们无法通过 npm install -g
安装我们的程序包,那我们要怎么测试呢?这个时候我们可以使用 npm link
(opens new window) 命令。
在项目根目录下运行
$ npm link
它会将我们的程序包 chinesize
链接到 {prefix}/lib/node_modules/chinesize
。同时还会将包中的 bin/chinesize.js
链接到 {prefix}/bin/chinesize
。因为都是符号链接,所以项目的任何改动直接影响 chinesize
命令,不需要重新运行 npm link
。
prefix
是 npm global prefix,可以通过 npm config get prefix
查看
我们现在可以在 chinesize.js
写代码,然后通过 chinesize
命令直接运行了。
#!/usr/bin/env node
console.log('Hello, Chinesize!');
2
3
$ chinesize
Hello, Chinesize!
2
# 参数解析
在实现汉化功能之前,我们先看看怎么解析命令行参数。一般的 Node.js 命令行工具都会有参数,比如:
$ jscodeshift fileA --transform myTransforms --dry
可以通过 process.argv
访问 Node.js 命令行工具的参数
// chinesize.js
console.log(process.argv);
2
运行下面的命令
$ chinesize file name=path --type=html --log output.txt --silent
输出:
[
'/Users/cp3hnu/.nvm/versions/node/v16.14.0/bin/node',
'/Users/cp3hnu/Documents/mine/chinesize/index.js',
'file',
'name=path',
'--type=html',
'--log',
'output.txt',
'--silent'
]
2
3
4
5
6
7
8
9
10
从输出中可以看出 process.argv
是个数组,参数以空格分隔,第一个参数 argv[0]
表示 node
的路径(我使用 nvm),第二个参数 argv[1]
表示运行程序的文件路径,后面是以空格分隔的参数列表。
从上面的输出我们可以看出,解析 process.argv
会比较麻烦。比如,哪些是参数,哪些是选项?选项名称前面要不要 "--"
?选项名与选项值之间要不要 "="
连接?布尔值的选项要怎么表示?等等...
所以我们使用第三方库来解析参数。我们这里使用了 commander
(opens new window),yargs
(opens new window) 是另一个不错的选择。
# commander
# 安装
$ npm install commander
# 教程
commander
支持选项参数和普通参数,选项参数支持长名称( --
)和短标记( -
),以空格分隔。
// index.js
import { program } from 'commander';
program
.option('--first')
.option('-s, --separator <char>');
program.parse();
const options = program.opts();
const limit = options.first ? 1 : undefined;
console.log(program.args[0].split(options.separator, limit));
2
3
4
5
6
7
8
9
10
11
12
commander
使用 program.parse()
方法解析命令行参数。
program.opts()
返回解析后的选项参数,它是一个对象({选项名: 选项值}
)program.args
返回没有被解析成选项的剩余参数,它是一个数组。
commander
支持下面选项参数的写法
serve -p 80
serve -p80
serve --port 80
serve --port=80
2
3
4
commander
还自带了帮助文档和错误提示。
$ node index -s / --fits a/b/c
error: unknown option '--fits'
(Did you mean --first?)
$ node index -s / --first a/b/c
[ 'a' ]
$ node index --help
Usage: index [options]
Options:
--first
-s, --separator <char>
-h, --help display help for command
2
3
4
5
6
7
8
9
10
11
12
13
14
commander
也支持子命令,像 git clone
一样。比如,下面定义了一个 string-util split
的子命令
// string-util.js
import { Command } from 'commander';
const program = new Command();
program
.name('string-util')
.description('CLI to some JavaScript string utilities')
.version('1.0.0');
program
.command('split')
.description('Split a string into substrings and display as an array')
.argument('<string>', 'string to split')
.option('--first', 'display just the first substring')
.option('-s, --separator <char>', 'separator character', ',')
.action((str, options) => {
const limit = options.first ? 1 : undefined;
console.log(str.split(options.separator, limit));
});
program.parse();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
commander
的每个子命令都要求有一个操作处理程序,可以是 action
函数,也可以是单独的可执行文件
$ node string-util.js help split
Usage: string-util split [options] <string>
Split a string into substrings and display as an array.
Arguments:
string string to split
Options:
--first display just the first substring
-s, --separator <char> separator character (default: ",")
-h, --help display help for command
$ node string-util.js split --separator=/ a/b/c
[ 'a', 'b', 'c' ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
commander
的详细教程,请参考 commander
的官方文档 (opens new window)
# 使用
回到我们的命令行工具 chinesize。Angular 的汉化分为两步:提取和替换,所以我们定义了两个子命令:extract
和 repalce
。
// bin/chinesize.js
#!/usr/bin/env node
import { Command, Option } from 'commander';
const program = new Command();
import { extract } from './src/extract.js';
program
.name('chinesize')
.description('CLI to convert English Angular project to Chinese')
.version('1.0.0');
program
.command('extract')
.description('Extract English texts of Angular project')
.argument('<dir>', 'directory of Angular project')
.addOption(
new Option('-t, --type <type>', 'file type')
.choices(['html', 'js'])
.makeOptionMandatory(false)
)
.option('-o, --output <filePath>', 'file path for writing the extracted English text')
.action((dir, options) => {
extract(dir, options.type, options.output);
});
program
.command('replace')
.description('Replace English texts of Angular project to Chinese')
.usage('<dir> [options]')
.argument('<dir>', 'directory of Angular project')
.addOption(
new Option('-t, --type <type>', 'file type')
.choices(['html', 'js'])
.makeOptionMandatory(false)
)
.option('-i, --input <filePath>', 'path of file for reading the Chinese text')
.option('-p, --prettier-config <configFilePath>', 'path of config file for prettier')
.action((dir, options) => {
replace(dir, options.type, options.input, options.prettierConfig);
});
program.parse();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
extract
有 1 个参数和 2 个选项:
dir
:汉化的 Angular 项目目录--type <type>
:要转换的文件类型,是html
文件还是js/ts
文件。如果没有提供,则同时转换两者--output <filePath>
:输出文件路径,提取的英文文本将写入这个文件。如果没有提供,默认是 Angular 项目目录下的chinesize/texts-to-translate-{html|ts}.json
文件
replace
有 1 个参数和 3 个选项:
dir
:汉化的 Angular 项目目录--type <type>
:要转换的文件类型,是html
文件还是js/ts
文件。如果没有提供,则同时转换两者--input <filePath>
:中英文翻译的文件路径。如果没有提供,默认是extract
生成的文件路径--prettier-config <configFilePath>
:prettier
配置文件路径。如果没有提供,则不使用prettier
格式化代码
# 优化输出
Node.js 提供了 console
(opens new window) 模块输出日志或者错误信息到控制台,但是错误信息和日志是一样的样式,用户不好发现,比如提示用户给的项目目录不存在,输出错误如下:
希望显示红色的字体,这样比较醒目。Node.js 支持 escape sequences (opens new window) 来设置字体颜色
console.log('\x1b[31m%s\x1b[0m',`Error: "${dir}" is not exists`);
ansi escape code 以
\x1b
或者\033
开头,它们表示ESC
(27),所以叫做 ansi escape code。
但是这种写法比较丑陋,所以 chalk
(opens new window) 应运而生
yoctocolors
(opens new window) 是一个轻量级的选择
# chalk
给控制台输出添加样式
# 安装
$ npm install chalk
# 使用
chalk
API:
chalk.<style>[.<style>...](string, [string...])
chalk
提供了多个设置样式的函数,比如 red
、bold
、bgRed
等等,它们可以级联使用,更多详情请参考 chalk
官方文档 (opens new window)
现在我们使用 chalk
来设置我们的错误信息,比如:
console.log(chalk.red.bold(`Error: "${dir}" is not exists`));
还可以使用
chalk-animation
(opens new window) 添加动画gradient-string
(opens new window) 添加渐变色terminal-link
(opens new window) 添加可点击的链接
# 发布
在测试、验证功能完全正确之后,就可以发布到 npm 上了。
首先做一些准备工作:
- 在 npm 官网 (opens new window)上创建 npm 账号
- 在 github 上创建 repo,并将代码上传到 github
- 修改
package.json
文件,填写repository
、homepage
、bugs
等字段
{
"name": "chinesize",
"version": "0.0.1",
"description": "Convert English React/Angular/HTML project to Chinese",
"repository": {
"type": "git",
"url": "https://github.com/cp3hnu/chinesize"
},
"homepage": "https://github.com/cp3hnu/chinesize#readme",
"bugs": "https://github.com/cp3hnu/chinesize/issues",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"chinesize": "bin/chinesize.js"
},
"keywords": [
"chinesize",
"angular",
"react",
"html",
"posthtml"
],
"author": "cp3hnu",
"license": "ISC",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"posthtml": "^0.16.6",
"prettier": "^3.3.3"
},
"engines": {
"node": ">=16"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
完成上面的准备工作之后,就可以发布了
- 首先检查 npm 源,如果是淘宝源,则需要改回 npm 源
# 查看npm 镜像源地址
$ npm config get registry
# 设置 npm 默认源
$ npm config set registry https://registry.npmjs.org/
2
3
4
5
也可以使用
nrm
(opens new window) 工具
- 查看哪些文件被打包上传
$ npm pack --dry-run
可以通过 .gitignore
或者 .npmignore
排除不需要上传的文件,也可以通过设置 package.json
的 files
指定需要打包哪些文件
- 查询是否已经存在重名的包
$ npm search chinesize
No matches found for "chinesize"
2
- 登录 npm
$ npm login
如果是 npm v9,需要使用
npm login --auth-type legacy
- 发布
$ npm publish
Publishing to https://registry.npmjs.org/ with tag latest and default access
Authenticate your account at:
https://www.npmjs.com/auth/cli/6fa1a0c8-4fd1-4cf3-9895-6b00fa83e384
Press ENTER to open in the browser...
+ chinesize@0.0.1
2
3
4
5
6
7
8
- 测试
$ npm unlink chinesize
$ chinesize --help
$ npm install chinesize -g
$ chinesize --help
2
3
4
- 更新
完成代码修改之后,打开 package.json
文件,修改 version
字段,比如从 0.0.1
更新到 0.0.2
,然后通过 npm publish
。
也可以通过 npm version
(opens new window) 自动增加版本号,并且 commit
修改。
// 增加补丁号
$ npm version patch
// 增加次版本号
$ npm version minor
// 增加主版本号
$ npm version major
2
3
4
5
6
7
8
关于版本管理,请参考 Semantic Versioning 2.0.0 (opens new window)
# 实现效果
通过 sourcetree 查看英文文本是否被替换成了中文。
# 完整代码
github 地址:chinesize (opens new window)
npm 地址:chinesize (opens new window)
# References
posthtml
(opens new window)cheerio
(opens new window)htmlparser2
(opens new window)parse5
(opens new window)- AST Explorer (opens new window)
- How to build a CLI with Node.js (opens new window)
- 如何使用 Node.js 构建一个命令行应用(CLI) (opens new window)
- What the heck are CJS, AMD, UMD, and ESM in Javascript? (opens new window)
- CommonJs、AMD、UMD、ESM 模块化的区别 (opens new window)
- npm (opens new window)
nrm
(opens new window)commander.js
(opens new window)yargs
(opens new window)chalk
(opens new window)yoctocolors
(opens new window)picocolors
(opens new window)micromatch
(opens new window)inquirer
(opens new window)enquirer
(opens new window)prompts
(opens new window)isomorphic-git
(opens new window)git-js
(opens new window)nodegit
(opens new window)fs-extra
(opens new window)awesome-nodejs
(opens new window)- Semantic Versioning 2.0.0 (opens new window)