如何实现自己的cli工具

132 阅读1分钟

1. 初始化项目

  • npm init

  • 修改package.json

  "bin": {
    "winter_cli": "bin/index.js"
  }
  • 新增 bin/index.js
#!/usr/bin/env node  
//一定要写,表明了index.js是node可执行文件
console.log('test')
  • 执行 winter_cli 命令

2E123F22-1F77-4C7A-9648-B7E53B374A9F.png

说明还没有建立全局的winter_cli命令,我们可以用npm link链接到全局

image.png

此时,全局命令已经链接成功

2. 定义选项(类似npm -v、node -v这样的命令)

  • 安装commander库
    npm install commander
    Commander 负责将参数解析为选项和命令参数。
    PS: 关于Commander juejin.cn/post/713192…

  • 修改bin/index.js文件

#!/usr/bin/env node  
//一定要写,表明了index.js是node可执行文件

const { program } = require('commander')
function strToArr (value) {
    return value?.split(',')
}
program.version(`version is ${require('../package.json').version}`)
    .option('-d, --debug', '调试')
    .option('-l, --list <value>', '把字符串分割为数组', strToArr)
    .action((options, command) => {
        if(options.debug) {
            console.log('调试成功')
        }
        if(options.list !== undefined){
            console.log(options.list)
        }
    })
    .parse(process.argv)
  • 终端验证

image.png

3.设置子命令(类似npm run xxx)

  • 修改bin/index.js文件

image.png

program
    .command('create <filename>')
    .description('创建一个项目')
    .action((filename) => {
        console.log('项目名为:', filename)
    })
  • 终端验证

image.png

4. github拉取代码、设置用户询问和提示

  • 安装过程中需要用到的插件:
npm install commander  //负责将参数解析为选项和命令参数
npm install download-git-repo  //github api 拉取代码
npm install inquirer // 生成用户询问和提示 项目里面用的8.2.2版本,用更高版本会有如下报错,还未解决
npm install ora // 用了做拉取的loading 项目里面用2版本,更高版本也会报错

高版本报错如下:

image.png

  • 由于接下来的配置文件较多,不能都写在bin/index.js文件中,所以我们调整下目录结构

image.png

新增actions.js(写所有的action配置)、constants.js(常量配置,比如version、name)、create.js(拉取代码、生成用户提示、项目配置、选择项目等)、main.js(配置入口、遍历actions里面导出的配置)

//bin/index.js

#!/usr/bin/env node  
//一定要写,表明了index.js是node可执行文件
require('../src/main')
//actions.js

const actions = {
    create: {
        alias: 'crt',
        description: 'create a project', 
        examles: [
            'make-cli create <project>'
        ]
    },
    config: {
        alias: 'conf',
        description: 'config project variable',
        examles: [
            'make-cli config set <key> <value>',
            'make-cli config get <key>'
        ]
    },
    '*': {
        alias: '',
        description: 'command not found',
        examles: []
    }
}

module.exports = {
    actions
}
//constants.js
const { name, version } = require('../package.json')

module.exports = {
    name,
    version
}
//create.js
const download = require('download-git-repo')  //github api 拉取代码
const inquirer = require('inquirer')  //用户询问
const ora = require('ora') //实现loading效果

// key是给用户选择的名字,value对应github里面的项目名
const templateMap = {
    vite_react: 'vite_react_project_template',
    ui_library: 'ui-test',
    monorepo: 'monorepo-test'
}

const fetchTemplate = async (options, filename) => {
    const { template } = options
    const templateUrl = `drxiong/${templateMap[template]}`
    //根据template模板
    const loading = ora('fetching')
    loading.start()
    // ${process.cwd()} 表示当前目录
    console.log(templateUrl,`${process.cwd()}/${filename}`)
    download(templateUrl, `${process.cwd()}/${filename}`, err => {
        if(err){
            console.log('err:', err)
            return
        }
        loading.succeed('success!!!')
    })
}

const templateOptions = ['vite_react', 'ui_library', 'monorepo']
const styleOptions = ['无', 'less', 'sass']

// 拉取代码之前询问配置,之后再根据用户选择

const askOptions = async () => {
    const { template } = await inquirer.prompt([
        {
            type: 'list',
            name: 'template',
            message: '请选择一个你要创建的项目',
            choices: templateOptions
        }
    ])

    return {
        template
    }
}

module.exports = async (projectName) => {
    const result = await askOptions()
    fetchTemplate(result, projectName)
}

//main.js
const { program } = require('commander')
const { version } = require('./constants')
const { actions } = require('./actions')
const path = require('path')

// 遍历配置
Reflect.ownKeys(actions).forEach(action => {
    program.command(action)
           .alias(actions[action].alias)
           .description(actions[action].description)
           .action(()=>{
             if(action === '*') {
                console.warn(actions[action].description)
             } else {
                // 寻找对应操作的文件
                const actionPath = path.join(__dirname, action)
                // 导出文件函数
                const func = require(actionPath)
                // 执行函数,并传参
                typeof func === 'function' && func(...process.argv.slice(3))
             }
           })  
})

program.version(version)
        .parse(process.argv) // process.argv 命令行参数
  • 终端测试
  1. 现在winter-cli项目的根目录执行npm link,将winter-cli命令链接到全局
  2. 在需要生成新项目模板的目录执行:winter_cli create first-project

image.png

image.png

  1. 此时到xy目录会发现,一个名为first-project的项目已经生成,且运用了模板vite_react, 代码与github上的vite_react_project_template一致,是一个用vite+pnpm搭建的具有单元测试、eslint等基础功能的react项目模板。

至此这个工具在本地的功能已经完善,下一步就是发到npm上。

5. npm发包

  • 手动发包:
npm login
npm publish
  • 自动发包
    • 配置SSH(自动发包的时候需要自动修改版本号,修改了package.json文件,同时需要把修改后的package.json提交到github上,这时候需要使用ssh鉴权,所以需要配置SSH)

      • 生成一对名叫runner的公钥私钥对(为了避免与本机的id_rsa冲突,所以重新起了一个名字叫runner) image.png

      • 查看公钥、私钥

        image.png

      • 将公钥拷贝出来配置到github的公钥里面

      image.png

      • 将私钥拷出来放到项目的secrets里面

image.png

  • 配置npm的Access Tokens

image.png

image.png

image.png

复制token

image.png

到github项目里面配置token

image.png

  • 新增publish.yml文件
name: Publish Smarty-ui-vite To Npm

# push main分支的时候执行
on:
  push:
    branches: [main]

jobs:
  publish:
    runs-on: ubuntu-latest # 一个环境,相当于一台机器
    steps:
      - uses: actions/checkout@v4  # 拉取代码
      - name: Setup Node.js
        uses: actions/setup-node@v4  # 设置node版本
        with:
          node-version: '18'
          registry-url: 'https://registry.npmjs.org/'
          always-auth: true
      - name: Debug .npmrc
        run: cat .npmrc

      - name: Debug npm version
        run: npm --version

      - name: Install semver  # 安装设置版本的依赖
        run: npm install semver

      - name: set ssh key # 临时设置 ssh key 【RUNNER_TOKEN需要和github上的私钥名对应】 【1.家目录下新建.ssh文件,2.将github上的私钥写入.ssh/id_rsa,3.修改权限,4.收集 github.com 的公钥信息,并将这些信息追加到 /home/runner/.ssh/known_hosts 文件中】
        run: |
          mkdir -p /home/runner/.ssh/  
          echo "${{secrets.RUNNER_TOKEN}}" > /home/runner/.ssh/id_rsa
          chmod 600 /home/runner/.ssh/id_rsa
          ssh-keyscan "github.com" >> /home/runner/.ssh/known_hosts
          

      - name: Set up Git
        run: |
          git config --global user.email "1763303455@qq.com"
          git config --global user.name "drxiong"

      - name: Auto version increment # 获取当前的version, 并修改版本的修订号,然后将修改后的package.json提交到github上
        env: # 这里注意commit的描述里面有 [skip ci] 这个不能少,是为了避免在这一次提交过程继续触发这个脚本,导致陷入循环的
          NODE_AUTH_TOKEN: ${{ secrets.RUNNER_TOKEN }}
        run: |
          current_version=$(node -p "require('/home/runner/work/winter_cli/winter_cli/package.json').version")
          new_version=$(node -p "const semver = require('semver'); semver.inc('$current_version', 'patch')")
          npm version --no-git-tag-version patch
          git add .
          git commit -m "Auto version increment [skip ci]"
          git push git@github.com:drxiong/winter_cli.git
        

      - name: Check environment variable
        run: |
          if [ -z "${{ secrets.WINTER_CLI_NPM_AUTH_TOKEN }}" ]; then
            echo "WINTER_CLI_NPM_AUTH_TOKEN is not set."
          else
            echo "WINTER_CLI_NPM_AUTH_TOKEN is set."
          fi

      - name: Publish package # 发包,这里的WINTER_CLI_NPM_AUTH_TOKEN需要分别在npm和github上设置
        run: npm set registry https://registry.npmjs.org/ && npm publish --access public --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{secrets.WINTER_CLI_NPM_AUTH_TOKEN}}

这样在push main分支的时候,就会自动发布npm包了,并且每次会将version的修订号自动加1