脚手架框架之yargs高级应用
1 )高级应用概述
- 现在还用 xyzcli 这个脚手架,继续在这个项目中来看yargs的高级用法
- 在 yargs 文档中, 给出了复杂应用的方式,这里做下详解
- https://www.npmjs.com/package/yargs?activeTab=readme#complex-example
- 这里主要关注 ↓
command
recommendCommands
fail
2 )command 应用案例
2.1 官方示例
- 复杂应用案例,自定义 command
#!/usr/bin/env node const yargs = require('yargs/yargs') const { hideBin } = require('yargs/helpers')yargs(hideBin(process.argv))// 注意 command的 四个参数.command('serve [port]', 'start the server', (yargs) => {return yargs.positional('port', {describe: 'port to bind on',default: 5000})}, (argv) => {if (argv.verbose) console.info(`start server on :${argv.port}`)serve(argv.port)}).option('verbose', {alias: 'v',type: 'boolean',description: 'Run with verbose logging'}).parse()
- 这里 command 的四个参数分别是
- 1 )command + options, 这里
serve [port]
- 这里的serve是自定义的命令,如: $
xyzcli serve
- 这里的
[port]
是 option,如: $xyzcli serve --port
- 这里的serve是自定义的命令,如: $
- 2 )描述,这里是
start the server
- 用于对前面命令和配置的补充描述
- 3 )builder函数,这里是一个回调
- builder是指在运行command之前做的一些事情
- 这里会传入 yargs 对象
- 比如在这里可以定义在这个 serve 命令中用到的 option
- 这里定义了一个port参数,并给它一个5000的默认值
- 4 )handler函数,用于具体执行 command 的行为
- 比如这个 serve 命令启动了一个http的服务
- 这个http服务就在这个 handler 函数中定义
- 1 )command + options, 这里
- 这里 command 的四个参数分别是
2.2 在xyzcli中配置, 这里接上文走一个全量的代码
#!/usr/bin/env nodeconst yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const dedent = require('dedent');const arg = hideBin(process.argv);
const cli = yargs(arg)cli.usage('Usage: xyzcli [command] <options>').demandCommand(1, 'A command is required. Pass --help to see all available commands and options.').strict().alias('h', 'help').alias('v', 'version')// .alias('d', 'debug').wrap(cli.terminalWidth()).epilogue(dedent`Welcome to use xyzcli command line.We hope you guys work happily every day!For more information, please visit: https://xxx.com/xxx/xyzcli`).options({debug: {type: 'boolean',describe: 'Bootstrap debug mode',alias: 'd' // 这里添加了,上面就可以忽略了,推荐写在这里}}).option('registry', {type: 'string',describe: 'Define global registry',alias: 'r',// hidden: true, // 这个就不会出现在提示中,但是可以作为开发调试使用 --ci}).group(['debug'], '开发选项:').group(['registry'], '其他选项:').command('init [name]', 'Init your project', (yargs) => {yargs.option('name', {type: 'string',describe: 'Name of a project',alias: 'n',})}, (argv) => {console.log(argv);}).argv;
- 执行 $
xyzcli init
,查看输出{ _: [ 'init' ], '$0': 'xyzcli' }
- 执行 $
xyzcli init --name xx
,查看输出{ _: [ 'init' ], name: 'xx', '$0': 'xyzcli' }
- 执行 $
xyzcli init -h
,查看输出xyzcli init [name]Init your project开发选项:-d, --debug Bootstrap debug mode [布尔]其他选项:-r, --registry Define global registry [字符串]选项:-n, --name Name of a project [字符串]-h, --help 显示帮助信息 [布尔]-v, --version 显示版本号 [布尔]
- 可见最上面出现一则帮助提示
xyzcli init [name]Init your project
- 可见最上面出现一则帮助提示
- 执行
xyzcli init -d -r npm -n x-project
, 查看输出{_: [ 'init' ], // _ 里面就是注册的 command 命令// d 和 debug 存在这两个option, 则为trued: true,debug: true,// r 和 registry 也同样r: 'npm',registry: 'npm',// n 是 name,项目名称为 x-projectn: 'x-project',name: 'x-project',// $0 就是脚手架命令'$0': 'xyzcli' }
- 注意,这种定义下,注意 alias 别名不能重复
- 如果重复,覆盖了,就会出问题
2.3 参考 lerna 工程示例
- 参考:https://github.com/lerna/lerna/blob/main/packages/lerna/src/index.ts
// Bundled import { lernaCLI } from "@lerna/core"; import changedCmd from "@lerna/commands/changed/command"; import cleanCmd from "@lerna/commands/clean/command"; import diffCmd from "@lerna/commands/diff/command"; import execCmd from "@lerna/commands/exec/command"; import importCmd from "@lerna/commands/import/command"; import infoCmd from "@lerna/commands/info/command"; import initCmd from "@lerna/commands/init/command"; import listCmd from "@lerna/commands/list/command"; import publishCmd from "@lerna/commands/publish/command"; import runCmd from "@lerna/commands/run/command"; import versionCmd from "@lerna/commands/version/command";import addCachingCmd from "./commands/add-caching/command"; import repairCmd from "./commands/repair/command"; import watchCmd from "./commands/watch/command";// Evaluated at runtime to grab the current lerna version const pkg = require("../package.json");module.exports = function main(argv: NodeJS.Process["argv"]) {const context = {lernaVersion: pkg.version,};const cli = lernaCLI().command(addCachingCmd).command(changedCmd).command(cleanCmd).command(createCmd).command(diffCmd).command(execCmd).command(importCmd).command(infoCmd).command(initCmd).command(listCmd).command(publishCmd).command(repairCmd).command(runCmd).command(watchCmd).command(versionCmd);applyLegacyPackageManagementCommands(cli);return cli.parse(argv, context); };
- 按照上述这种用法,找其中一条
.command(listCmd)
- 定位到
import listCmd from "@lerna/commands/list/command";
- https://github.com/lerna/lerna/blob/main/packages/lerna/src/commands/list/command.ts 发现内容很少
- 再次查看全局配置: https://github.com/lerna/lerna/blob/main/tsconfig.base.json
"@lerna/commands/list": ["libs/commands/list/src/index.ts"],
- 定位到这里 https://github.com/lerna/lerna/blob/main/libs/commands/list/src/command.ts
import { filterOptions, listableOptions } from "@lerna/core"; import type { CommandModule } from "yargs";/*** @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module*/ const command: CommandModule = {command: "list",aliases: ["ls", "la", "ll"],describe: "List local packages",builder(yargs) {listableOptions(yargs);return filterOptions(yargs);},async handler(argv) {return (await import(".")).factory(argv);}, };export = command;
- 可见这个 command 是一个对象,对象里有4项参数
- 以上这个,是 Lerna 对yargs的内部应用,同样,在自己的脚手架中试验一下
const cli = yargs(arg)cli.usage('Usage: xyzcli [command] <options>').demandCommand(1, 'A command is required. Pass --help to see all available commands and options.').strict().alias('h', 'help').alias('v', 'version')// .alias('d', 'debug').wrap(cli.terminalWidth()).epilogue(dedent`Welcome to use xyzcli command line.We hope you guys work happily every day!For more information, please visit: https://xxx.com/xxx/xyzcli`).options({debug: {type: 'boolean',describe: 'Bootstrap debug mode',alias: 'd' // 这里添加了,上面就可以忽略了,推荐写在这里}}).option('registry', {type: 'string',describe: 'Define global registry',alias: 'r',// hidden: true, // 这个就不会出现在提示中,但是可以作为开发调试使用 --ci}).group(['debug'], '开发选项:').group(['registry'], '其他选项:').command('init [name]', 'Init your project', (yargs) => {yargs.option('name', {type: 'string',describe: 'Name of a project',alias: 'n',})}, (argv) => {console.log(argv);})// 注意,这里.command({command: 'list',aliases: ['ls', 'la', 'll'],describe: 'List local package',builder: (yargs) => {},handler: (argv) => { console.log(argv); }}).argv;
- 执行 $
xyzcli list
, 查看输出{ _: [ 'list' ], '$0': 'xyzcli' }
- 执行 $
xyzcli ls
, 查看输出{ _: [ 'ls' ], '$0': 'xyzcli' }
- 执行 $
xyzcli ll
, 查看输出{ _: [ 'll' ], '$0': 'xyzcli' }
- 执行 $
xyzcli la
, 查看输出{ _: [ 'la' ], '$0': 'xyzcli' }
- 以上就是 command 的两种用法
3 )recommendCommands 应用案例
const cli = yargs(arg)cli.usage('Usage: xyzcli [command] <options>').demandCommand(1, 'A command is required. Pass --help to see all available commands and options.').strict().recommendCommands() // 注意这里.alias('h', 'help').alias('v', 'version')// .alias('d', 'debug').wrap(cli.terminalWidth()).epilogue(dedent`Welcome to use xyzcli command line.We hope you guys work happily every day!For more information, please visit: https://xxx.com/xxx/xyzcli`).options({debug: {type: 'boolean',describe: 'Bootstrap debug mode',alias: 'd' // 这里添加了,上面就可以忽略了,推荐写在这里}}).option('registry', {type: 'string',describe: 'Define global registry',alias: 'r',// hidden: true, // 这个就不会出现在提示中,但是可以作为开发调试使用 --ci}).group(['debug'], '开发选项:').group(['registry'], '其他选项:').command('init [name]', 'Init your project', (yargs) => {yargs.option('name', {type: 'string',describe: 'Name of a project',alias: 'n',})}, (argv) => {console.log(argv);}).command({command: 'list',aliases: ['ls', 'la', 'll'],describe: 'List local package',builder: (yargs) => {},handler: (argv) => { console.log(argv); }}).argv;
- 上面加上
.recommendCommands()
之后, 尝试执行 $xyzcli l
, 查看输出Usage: xyzcli [command] <options>命令:xyzcli init [name] Init your projectxyzcli list List local package [aliases: ls, la, ll]开发选项:-d, --debug Bootstrap debug mode [布尔]其他选项:-r, --registry Define global registry [字符串]选项:-h, --help 显示帮助信息 [布尔]-v, --version 显示版本号 [布尔]Welcome to use xyzcli command line. We hope you guys work happily every day!For more information, please visit: https://xxx.com/xxx/xyzcli是指 ls?
- 看这最后,
是指 ls?
, 这个命令就是当你输入不全时,尝试对你进行提醒
- 看这最后,
4 )fail 应用案例
cli.usage('Usage: xyzcli [command] <options>').demandCommand(1, 'A command is required. Pass --help to see all available commands and options.').strict().recommendCommands()// 注意这里.fail((msg, err) => {console.log('msg: ', msg)console.log('err: ', err)}).alias('h', 'help').alias('v', 'version')// .alias('d', 'debug').wrap(cli.terminalWidth()).epilogue(dedent`Welcome to use xyzcli command line.We hope you guys work happily every day!For more information, please visit: https://xxx.com/xxx/xyzcli`).options({debug: {type: 'boolean',describe: 'Bootstrap debug mode',alias: 'd' // 这里添加了,上面就可以忽略了,推荐写在这里}}).option('registry', {type: 'string',describe: 'Define global registry',alias: 'r',// hidden: true, // 这个就不会出现在提示中,但是可以作为开发调试使用 --ci}).group(['debug'], '开发选项:').group(['registry'], '其他选项:').command('init [name]', 'Init your project', (yargs) => {yargs.option('name', {type: 'string',describe: 'Name of a project',alias: 'n',})}, (argv) => {console.log(argv);}).command({command: 'list',aliases: ['ls', 'la', 'll'],describe: 'List local package',builder: (yargs) => {},handler: (argv) => { console.log(argv); }}).argv;
-
尝试执行 $
xyzcli lxx
故意给一个没有的命令,查看输出结果msg: 是指 ls? err: undefined msg: 无法识别的选项:lxx err: undefined
- 这里输出了 2个 msg, 两个 err, 这里 err 都是 undefined
- 在实际应用中,参考 lerna, 如下
-
参考 lerna 中的应用 https://github.com/lerna/lerna/blob/main/libs/core/src/lib/cli.ts#L18
.fail((msg, err: any) => {// certain yargs validations throw strings :Pconst actual = err || new Error(msg);// ValidationErrors are already logged, as are package errorsif (actual.name !== "ValidationError" && !actual.pkg) {// the recommendCommands() message is too terseif (/Did you mean/.test(actual.message)) {// TODO: refactor to address type issues// eslint-disable-next-line @typescript-eslint/ban-ts-comment// @ts-ignorelog.error("lerna", `Unknown command "${cli.parsed.argv._[0]}"`);}log.error("lerna", actual.message);}// exit non-zero so the CLI can be usefully chainedcli.exit(actual.exitCode > 0 ? actual.exitCode : 1, actual); })