node cli &&创建一个手架的demo

关于process.argv

image-20211124145747228
  • process.argv 返回一个数组,其中包含node 启动时包含的输入参数
  • 数组第一项是node的执行路径,process.execPath。第二项是正在执行的js文件路径,第三项开始是输入的参数。

node 的这个运行时可以进行参数处理,但是开发一个cli程序需要更方便的参数处理方式。

commander.js 是一个比较出名的cli 参数处理库(中文参考文档)

commander参数处理

const program = require('commander')
const getHelp = () => {}
program.name('better-clone') // 命令的名称
       .version('0.0.1')// 版本
       .option('-v --verbose', 'verbosity that can be increased') // 参数的描述
program.command('clone <source> [description]') //子命令, source必填的参数,description选填的参数
       .option('-d --depths <level>', 'git clone depths')// 这个命令的缩写是什么,全称是什么
       .description('clone a repository into a newly created directory')
       .action((source, destination, cmdObj) => {
            console.log(`start clone from ${source} to ${destination} with depth ${cmdObj.depths}`);
       })
program.parse(process.argv)
  • .option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|分隔。
  • 通过.command()或.addCommand()可以配置命令,有两种实现方式:为命令绑定处理函数,或者将命令单独写成一个可执行文件

image-20211124145928650

执行时什么都不传会默认展示help命令,自动生成的。

执行结果:

image-20211124145949793

inquirer.js友好的交互

目标:

  • 命令过长一步一输入。
  • 配置错误给提示,从当前重新输入。
  • 对于敏感信息,不留痕迹

inquirer提供灵活的cli 交互方式,包括:

input number confirm list rowlist expand checkbox password editor …

并且兼容windows/OSX/linux, 不关心平台底层实现细节

使用例子:

const inquirer = require('inquirer')
inquirer
  .prompt([
    /* Pass your questions in here */
    { type: 'input', name: 'username', message: "What's ur name?" },
    { 
        type: 'checkbox', 
        name: 'gender', 
        message: "What's ur gender?", 
        choices: [ 'male', 'female' ]
    },
    { 
        type: 'number', 
        name: 'age', 
        message: 'How old are u?',
        validate: input => Number.isNaN(Number(input)) 
            ? 'Number Required!' : true 
        },
    { 
        type: 'password', 
        name: 'secret', 
        message: 'Tell me a secret.', 
        mask: 'x' 
    }
  ])
  .then(answers => {
    console.log(`Answers are:\n ${JSON.stringify(answers)}`)
  })
  .catch(error => {
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else when wrong
    }
  })

chalk 高亮显示

const chalk = require('chalk');
const log = console.log;
 
// Combine styled and normal strings
log(chalk.blue('\nHello') + ' World' + chalk.red('!\n'));
 
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!\n'));
 
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz\n'));
 
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!\n'));

效果:

image-20211124150103932

shelljs、execa调用其他程序

shelljs

Shelljs是Node.js下的脚本语言解析器,具有丰富且强大的底层操作(Windows/Linux/OS X)权限

本质就是基于node的一层命令封装插件,让前端开发者可以不依赖linux也不依赖类似于cmder的转换工具,而是直接在我们最熟悉不过的javascript代码中编写shell命令实现功能。可以同步获得命令结果

var shell = require('shelljs');
 
if (!shell.which('git')) {
  shell.echo('Sorry, this script requires git');
  shell.exit(1);
}
 
// Copy files to release dir
shell.rm('-rf', 'out/Release');
shell.cp('-R', 'stuff/', 'out/Release');
 
// Replace macros in each .js file
shell.cd('lib');
shell.ls('*.js').forEach(function (file) {
  shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
  shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file);
  shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file);
});
shell.cd('..');
 
// Run external tool synchronously
if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
  shell.echo('Error: Git commit failed');
  shell.exit(1);
}

execa

  • 结果promise化
  • 跨平台支持shebang
  • 获取进程结束信号
  • 优雅退出
  • 更好的windows支持,比如路径反斜杠
const execa = require('execa');

(async () => {
	const {stdout} = await execa('echo', ['unicorns']);
	console.log(stdout);
	//=> 'unicorns'
})();

什么是shebang?

image-20211124150237252

告诉bash解释器要用node 去执行而不是shell

创建一个脚手架的demo

需求:设计一个脚手架,根据命令选择不同的模板,按照指定的参数(工程名、作者名)在指定的路径生成一个样板工程

拆解需求:

  • 参数的输入,结果的输出:commander.js inquirer.js chalk.js
  • 模板在哪里维护:git仓库
  • 如何获取模板:git clone 使用execa或者shell.js调用
#! /usr/bin/env node
/* 
    创建vue或者react的模板,并推送到指定远程仓库
    1、交互选择模板类型
    2、拉取模板代码到指定路径
    3、根据输入的工程名字、作者名字修改package.json文件
    4、推送代码到远程仓库
*/
const program = require('commander')
const chalk = require('chalk');
const inquirer = require('inquirer')
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const log = console.log;

const inputConfig = [
    { 
        type: 'input', 
        name: 'path', 
        message: "What's your output path?", 
    },
    { 
        type: 'input', 
        name: 'packageName', 
        message: "package name:",
        validate: function (v) {
            return v !=='' && typeof v === 'string'
        } 
    },
    { type: 'input', name: 'author', message: "author:" },
    { type: 'input', name: 'description', message: "description:" },
    { 
        type: 'list', 
        name: 'type', 
        message: "choose a template type:", 
        choices: [ 'vue', 'react' ]
    }
]

// 模板仓库的地址
const repositoryAddress = {
    vue: 'vue-template.git 地址',
    react: 'react-template.git 地址'
}
// 拉取代码
const cloneRepository = (repositoryPath, targetPath) => {
    log(chalk.blue('clone start ...'))
    //判定git命令是否可用
    if (!shell.which('git')) {
        shell.echo('Sorry, this script requires git');
        shell.exit(1);
    }

    // 执行git clone
    if (shell.exec(`git clone ${repositoryPath} ${targetPath}`).code !== 0) {
        shell.echo(`Error: Git clone ${repositoryPath} failed`);
        shell.exit(1);
    }
    log(chalk.blue('clone completed ...'))
}

// 修改package.json
const updatePkg = (res) => {
    // 没有指定路径,用默认路径
    const originName = res.type === 'vue'? 'vue-template' : 'react-template';
    res.path = res.path?res.path: originName;
    // 当前路径
    let currentPath = process.cwd();
    // 处理文件
    shell.cd(path.resolve(currentPath, res.path) )
    let _packageJson = JSON.parse(fs.readFileSync('./package.json') || {})
    _packageJson.name = res.packageName;
    _packageJson.author = res.author;
    _packageJson.description = res.description;

    fs.writeFileSync('./package.json', JSON.stringify(_packageJson, null, 2), function (err) {
        if (err) log(chalk.red(err));
    });
    shell.cd(currentPath)
    log(chalk.blue('修改package.json文件完毕 ...'))
}

// 提交到远程仓库
const pushRepository = (res) => {
    // 当前路径
    let currentPath = process.cwd();
    // 处理文件
    shell.cd(path.resolve(currentPath, res.path) )

    log(chalk.yellow(process.cwd()))

    log(chalk.blue('开始推送仓库 ...'))
    //判定git命令是否可用
    if (!shell.which('git')) {
        shell.echo('Sorry, this script requires git');
        shell.exit(1);
    }

    if (shell.exec('git init').code !== 0) {
        shell.echo(`Error: [git init] failed`);
        shell.exit(1);
    }

     if (shell.exec('git add .').code !== 0) {
        shell.echo(`Error: [git add .] failed`);
        shell.exit(1);
    }

     if (shell.exec(`git commit -m "init"`).code !== 0) {
        shell.echo(`Error: [git commit -m "init"] failed`);
        shell.exit(1);
    }

    if (shell.exec(`git remote remove origin`).code !== 0) {
        shell.echo(`Error: [git remote remove origin] failed`);
        shell.exit(1);
    }

     if (shell.exec(`git remote add origin ssh://git@xxx.com/${res.packageName}.git`).code !== 0) {
        shell.echo(`Error: [git remote add origin ssh://git@xxx.com/${res.packageName}.git] failed`);
        shell.exit(1);
    }


    if (shell.exec('git push -u origin master').code !== 0) {
        shell.echo(`Error: [git push -u origin master] failed`);
        shell.exit(1);
    }

    log(chalk.blue('推送仓库完毕 ...'))

    shell.cd(currentPath)
}

const generateProject = (res, repositoryAddress) => {
    cloneRepository(repositoryAddress, res.path)
    updatePkg(res);
    pushRepository(res);
}

program
    .name('create-test-cli')
    .version('1.0.5')
    .description('创建vue或者react的模板,并推送到指定远程仓库')
    .command('create')
    .action(()=> {
        
        inquirer
            .prompt(inputConfig).then(res => {

                switch (res.type) {
                    case "vue":
                        generateProject(res, repositoryAddress.vue);
                        break;
                    case "react":
                        generateProject(res, repositoryAddress.react);
                        break;
                
                    default:
                        break;
                }
            })
            .catch(error => {
                log(chalk.red(error))
        })
})

program.parse(process.argv)

notes:

package.json需要添加执行路径:

  "bin": {
    "create-test-cli": "./index.js"
  },

npm publish后:
安装:

npm i create-test-cli

使用:

create-test-cli create
  • output path 为仓库克隆下来的路径,规范符合git clone 的规范, 不传时在当前目录生成vue-template 或者react-template
  • name 必传, 为远程仓库名称,也是package.json 中的name
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值