项目地址

laddish/custom-cli
https://gitee.com/laddish/custom-cli

为什么?

image.png

必备模块

  1. "dependencies": {
  2. "axios": "^0.21.1",
  3. "chalk": "^4.1.0",
  4. "commander": "^7.2.0",
  5. "download-git-repo": "^3.0.2",
  6. "fs-extra": "^9.1.0",
  7. "inquirer": "^8.0.0",
  8. "ora": "^5.4.0"
  9. }

axios用于获取仓库repo和tag
chalk用于输入彩色字体
commander命令行输入输出模块
download-git-repo跟俊repo和tag下载
fs-extra增强版的fs模块,可以判断目录是否存在
inquirer用于命令行和用户的交互选择
ora用于创建等待的loading

思路

  • 先创建可执行的脚本 #! /usr/bin/env node
  • 配置package.json中的bin字段 配置别名链接
  • npm link 链接到本地环境 产生全局执行命令 C:\nodejs\npmglobal
    • npm unlink 取消软连接
    • npm link —force 强制链接
    • link相当于将当前本地模块链接到npm目录下 这个npm目录可以直接访问 所以当前包就可以直接访问了
  1. up to date in 1.274s
  2. found 0 vulnerabilities
  3. C:\nodejs\npmglobal\lad -> C:\nodejs\npmglobal\node_modules\lad\bin\lad
  4. C:\nodejs\npmglobal\node_modules\lad -> C:\mycode\custom-cli
  1. 配置可执行命令
    1. commander

  2. 做一个命令行交互的功能
    1. inquirer

  3. 将模板下载下来
    1. download-git-repo
  4. 根据用户的选择动态生成内容
    1. mitalsmith
  1. yarn add commander

目录结构

image.png

package.json

  1. {
  2. "name": "lad",
  3. "version": "1.0.0",
  4. "main": "index.js",
  5. "bin": {
  6. "lad": "./bin/lad",
  7. "lad-cli": "./bin/lad"
  8. },
  9. "scripts": {},
  10. "keywords": [],
  11. "author": "",
  12. "license": "MIT",
  13. "dependencies": {
  14. "axios": "^0.21.1",
  15. "chalk": "^4.1.0",
  16. "commander": "^7.2.0",
  17. "download-git-repo": "^3.0.2",
  18. "fs-extra": "^9.1.0",
  19. "inquirer": "^8.0.0",
  20. "ora": "^5.4.0"
  21. }
  22. }

bin/lad

创建后需要软连接npm link

#! /usr/bin/env node

console.log("lad-cli");

const program = require("commander");
const chalk = require("chalk");


program
  .command("create <app-name>")
  .description("Create a new project")
  .option("-f, --force", "overwrite target directory if it exists")
  .action((name, cmd) => {
    //动态调用create模块去创建
    // console.log(name, cmd); //需要提取这个cmd中的属性
    require("../lib/create")(name,cmd)
  });

program
  .command("config [value]")
  .description("inspect and modify the config")
  .option("-g, --get <path>", "get value from option")
  .option("-s, --set <path> <value>", "set option from config")
  .option("-d, --delete <path>", "delete option from config")
  .option("-l, --list", "list option")
  .action((value, cmd) => {
    console.log(value, cmd);
  });

program
  .command("ui")
  .description("start and open lad-cli ui")
  .option("-p, --port <path>", "Port used for the UI server")
  .action((cmd) => {
    //这里没有参数第一个
    console.log(cmd);
  });

program
  .version(`lad-cli@${require("../package.json").version}`)
  .usage(`<command> [option]`);

program.on("--help", function () {
  console.log();
  console.log(`Run ${chalk.cyan("lad <command> --help")} show details`);
  console.log();
});

//解析用户命令执行的参数
program.parse(process.argv);

//核心功能 1.创建项目 2.更改配置文件 3.ui界面 @vue/ui

request

//通过axios来获取结果

const axios = require("axios");

axios.interceptors.response.use((res) => {
  return res.data;
});

async function fetchRepoList() {
  //可以通过配置文件拉取不同仓库对应的用户下的文件
  return axios.get("https://api.github.com/orgs/zhu-cli/repos");
}

async function fetchTagList(repo) {
  //可以通过配置文件拉取不同仓库对应的用户下的文件
  console.log(`https://api.github.com/orgs/zhu-cli/${repo}/tags`)
  return axios.get(`https://api.github.com/repos/zhu-cli/${repo}/tags`);
}

module.exports = {
  fetchRepoList,
  fetchTagList
};

util

const ora = require("ora");

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time);
  });
}

//失败重新获取
async function wrapLoading(fn, msg, ...args) {
  //制作一个等待的loading
  const spinner = ora(msg);
  spinner.start(); //开启加载
  try {
    let repos = await fn(...args);
    spinner.succeed();
    return repos;
  } catch (error) {
    spinner.fail("request failed , refetch...");
    await sleep(1000);
    return wrapLoading(fn, msg, ...args);
  }
}

module.exports = {
  sleep,
  wrapLoading,
};

Creator

const inquirer = require("inquirer");
const { fetchRepoList, fetchTagList } = require("./request");
const { wrapLoading } = require("./util");
const downloadGitRepo = require("download-git-repo");
const util = require("util");
const path = require("path");

class Creator {

  constructor(projectName, targetDir) {
    //new的时候会调用构造函数
    this.name = projectName;
    this.target = targetDir;
    //此时这个方法就是一个promise方法
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }

  async fetchRepo() {
    //失败重新获取
    let repos = await wrapLoading(fetchRepoList, "waiting fetch template");
    // console.log(repos);s
    if (!repos) {
      return;
    }
    repos = repos.map((item) => item.name);
    let { repo } = await inquirer.prompt({
      name: "repo",
      type: "list",
      choices: repos,
      message: "choose a template to create project",
    });
    return repo;
  }

  async fetchTag(repo) {
    //根据repo拉取版本号
    let tags = await wrapLoading(fetchTagList, "waiting fetch tags", repo);
    if (!tags) {
      return;
    }
    // console.log(tags);
    tags = tags.map((item) => item.name);
    let { tag } = await inquirer.prompt({
      name: "tag",
      type: "list",
      choices: tags,
      message: "choose a tag to create project",
    });
    return tag;
  }

  async download(repo, tag) {
    //需要先拼接出下载路径
    let requestUrl = `zhu-cli/${repo}${tag ? "#" + tag : ""}`;
    //把资源下载到某个路径上(后续可以增加缓存功能)
    //放到系统目录中 模板 和用户的其他选择 生成结果放到当前目录下
    await wrapLoading(
      this.downloadGitRepo,
      `downloading template ${repo}@${tag}`,
      requestUrl,
      path.resolve(process.cwd(), `${repo}@${tag}`)
    );
    return this.target;
  }

  async create() {
    //创建
    //将模板down下来 download-git-repo
    // console.log(this.name);
    // console.log(this.target);
    //采用远程拉取 github 当前组织下的模板
    let repo = await this.fetchRepo();
    //再通过模板找到版本号
    let tag = await this.fetchTag(repo);
    console.log(repo, tag);
    //下载
    let downloadUrl = await this.download(repo, tag);

    //单独写个类去生成模板

    //编译模板
  }
}

module.exports = Creator;

create

const path = require("path");
const fs = require("fs-extra");

const inquirer = require("inquirer");
const chalk = require("chalk");

const Creator = require("./Creator");
module.exports = async function (projectName, options) {
  //   console.log(projectName, options);
  //创建项目
  const cwd = process.cwd(); //获取当前命令执行的工作目录
  //   console.log(cwd);
  const targetDir = path.join(cwd, projectName);
  if (fs.existsSync(targetDir)) {
    if (options.force) {
      //强制创建 删除已有的
      await fs.remove(targetDir);
    } else {
      //提示用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: "action",
          type: "list", //类型很丰富
          message: "Target directory already exists, pick an action",
          choices: [
            { name: "overwrite", value: "overwrite" },
            { name: "cancel", value: false },
          ],
        },
      ]);
      console.log(action);
      if (!action) {
        return;
      } else if (action == "overwrite") {
        console.log(`\r\n${chalk.yellow("Removing...")}\r\n`);
        await fs.remove(targetDir);
      }
    }
  }
  //创建项目
  const creator = new Creator(projectName, targetDir);
  creator.create();
};