创建脚手架工具,供命令行进行下载

23 阅读1分钟

Date: 2020-07-02

引入

我们在初始vue-cli的时候,经常使用如下的命令

vue init webpack test

那么是怎么实现的呢?如何自己搭建一个cli工具。

搭建具体流程

  • 新建一个 ***-cli 的文件夹,如 cpm-cli

  • npm init 初始化一个package.json

  • 编写自动化下载的脚本

    1. 使用 gitlab group 或 github organization 存储脚手架项目

    2. 使用 gitlab api 或 github api 读取项目列表 以 github 为例:https://api.github.com/orgs/${orgName}/repos

    3. 使用交互式命令供用户选择项目

    4. 使用 gitlab api 或 github api 下载脚手架 以 github 为例:github:${orgName}/${project}

    5. 重写package.json等文件

  • npm publish

npm包

  • commander:解析命令行
  • download-git-repo:下载git仓库
  • inquirer:交互式命令
  • axios:调用github接口
  • fs:读取和修改文件
  • ora:控制台输出loading
  • chalk:控制台输出不同颜色的文案
  • symbols:控制台console前面的类型

注意事项

  • 入口文件顶部:#!/usr/bin/env node
  • package.json添加:"bin": { "cpm-cli": "index.js" }
  • 安装依赖包时使用-S安装,不要使用-D安装
  • 调用program.parse(process.argv)时需要单独写,不要和其他的一起链式调用
  • download脚手架偶尔会报错“被拒绝连接connect ECONNREFUSED”,稍后重试即可
  • 需要配置github ssh key,否则无法download

代码

访问github

#!/usr/bin/env node
'use strict'

/**
 * cli脚手架搭建参考:
 *    https://developer.github.com/v3/
 *    https://www.jianshu.com/p/edeff714e8a3
 *    https://blog.csdn.net/weixin_38080573/article/details/97897767
 * 
 * 注意:
 *    入口文件顶部添加:#!/usr/bin/env node
 *    package.josn添加:"bin": { "cpm-cli": "index.js" }
 */

const program = require('commander');
const download = require('download-git-repo');
const inquirer = require('inquirer');
const axios = require('axios');
const fs = require('fs');
const ora = require('ora');
const chalk = require('chalk'); // green/success、red/error、yellow/tip
const symbols = require('log-symbols');

// config
const orgName = 'cpm-cli'; // github organization name

/**
 * 抓取github api获取脚手架列表
 */
function getOrgTemplateList () {
  return new Promise((resolve, reject) => {
    const spinner = ora(chalk.yellow('Request template list ...'));
    spinner.start();
    axios
      .get(`https://api.github.com/orgs/${orgName}/repos`)
      .then(res => {
        if (res.data && res.data.length) {
          const list = res.data.map(item => item.full_name.replace(`${orgName}/`, ''));
          spinner.succeed();
          resolve(list);
        } else {
          spinner.fail();
          reject();
        }
      })
      .catch((err) => {
        spinner.fail();
        reject(err);
      })
  })
}

/**
 * 交互式命令获取用户输入和选择
 * @param {Array} list 脚手架列表
 */
function showInquirer (list) {
  return new Promise((resolve, reject) => {
    inquirer
      .prompt([
        {
          type: 'input',
          message: 'please input package.json name',
          name: 'name',
          default: 'demo'
        },
        {
          type: 'input',
          message: 'please input package.json description',
          name: 'description',
          default: 'a demo project'
        },
        {
          type: 'input',
          message: 'please input package.json author',
          name: 'author',
          default: 'chenpengmin'
        },
        {
          type: 'list',
          message: 'please choice template',
          name: 'template',
          choices: list
        }
      ])
      .then((answers) => {
        resolve(answers);
      })
      .catch((err) => {
        reject(err);
      })
  })
}

/**
 * 下载脚手架
 * @param {Object} answers 
 * @param {String} projectName
 */
function downloadTemp (answers, projectName) {
  return new Promise((resolve, reject) => {
    const spinner = ora(chalk.yellow(`Downloading ${answers.template} template ...`));
    spinner.start();
    download(`github:${orgName}/${answers.template}`, projectName, { clone: true }, (err) => {
      if (err) {
        spinner.fail();
        reject(err);
        return;
      }
      spinner.succeed();
      resolve();
    })
  })
}

/**
 * 重写package.json,修改name、description、author等参数
 * @param {Object} answers 
 * @param {String} projectName 
 */
function rewritePackageJson (answers, projectName) {
  const { name, description, author } = answers;
  const packagePath = `${projectName}/package.json`;
  if (fs.existsSync(packagePath)) {
    const packageJson = fs.readFileSync(packagePath);
    const packageResult = JSON.stringify(
      Object.assign(
        {},
        JSON.parse(packageJson),
        {
          name,
          description,
          author
        }
      ),
      null,
      '\t'
    );
    fs.writeFileSync(packagePath, packageResult);
  }
}

/**
 * 主方法
 * @param {String} projectName
 */
async function main (projectName) {
  try {
    // 第一步:读取github organization列表
    const list = await getOrgTemplateList();

    // 第二步:交互式命令获取用户选择
    const answers = await showInquirer(list);

    // 第三步:下载脚手架
    await downloadTemp(answers, projectName);

    // 第四步:重写package.json
    rewritePackageJson(answers, projectName);

    console.log(symbols.success, chalk.green(`project ${projectName} was created successfully`));
  } catch (err) {
    console.log(symbols.error, chalk.red(err || 'something was wrong'));
  }
}

program
  .version(require('./package.json').version)
  .command('init <projectName>')
  .action((projectName) => {
    if (fs.existsSync(projectName)) {
      console.log(symbols.error, chalk.red(`project ${projectName} already exist`));
      return;
    }
    main(projectName);
  })
program.parse(process.argv);
{
  "name": "cpm-cli",
  "version": "0.1.7",
  "description": "cpm-cli",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "chenpengmin",
  "homepage": "https://github.com/cpm828/cpm-cli",
  "keywords": [
    "cli",
    "cpm",
    "cpm-cli"
  ],
  "license": "ISC",
  "bin": {
    "cpm-cli": "index.js"
  },
  "dependencies": {
    "axios": "^0.19.2",
    "chalk": "^4.1.0",
    "commander": "^5.1.0",
    "download-git-repo": "^3.0.2",
    "fs": "0.0.1-security",
    "inquirer": "^7.2.0",
    "log-symbols": "^4.0.0",
    "ora": "^4.0.4",
    "shell": "^0.5.0"
  }
}