美文网首页
动手搭建cli脚手架

动手搭建cli脚手架

作者: 郝同学1208 | 来源:发表于2025-08-13 16:06 被阅读0次

动手搭建cli脚手架

项目搭建

新建如下目录结构


cli目录结构

下面贴上具体文件代码
/bin/cli.js

#! /usr/bin/env node

// #! 符号的名称叫 Shebang,用于指定脚本的解释程序
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

// 用于检查入口文件是否正常执行
console.log("开始运行my-cli脚手架进行构建");

const { program } = require("commander");
const figlet = require("figlet");
program
  // 定义命令和参数
  .command("create <app-name>")
  .description("创建一个新项目")
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option("-f, --force", "强制创建,如果创建的目录存在则直接覆盖")
  .action((name, options) => {
    // 在 create.js 中执行创建任务
    require("../lib/create.js")(name, options);
  });

program
  // 配置版本号信息
  .version(`v${require("../package.json").version}`)
  .usage("<command> [option]");

program.on("--help", () => {
  // 使用 figlet 绘制 Logo
  console.log(
    "\r\n" +
      figlet.textSync("my-cli", {
        font: "Standard",
        horizontalLayout: "default",
        verticalLayout: "default",
        width: 80,
        whitespaceBreak: true,
      })
  );
});

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

/lib/Animate.js

const ora = require("ora");

class Animate {
  constructor(initText) {
    this.initText = initText;
    this.spinner = ora(initText);
  }

  start(text) {
    this.spinner.start(text);
  }

  change(text, color) {
    // color: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | boolean
    // 修改动画样式
    this.spinner.text = text;
    this.spinner.color = color || "cyan";
  }

  stop() {
    this.spinner.stop(); // 停止
  }

  message(msg, type) {
    // type: succeed✔ | fail✖ | warn⚠ | infoℹ
    this.spinner[type](msg);
  }
}
module.exports = Animate;

/lib/create.js

const path = require("path");
const fs = require("fs-extra");
const inquirer = require("inquirer");
const Generator = require("./Generator");
const ejs = require("ejs");

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd = process.cwd();
  // 需要创建的目录地址
  const targetPath = path.join(cwd, name);

  // 目录是否已经存在?
  if (fs.existsSync(targetPath)) {
    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetPath);
    } else {
      // 询问用户是否确定要覆盖
      let { action } = await inquirer.default.prompt([
        {
          type: "confirm",
          name: "action",
          message: "目标目录已存在,是否覆盖:",
        },
      ]);

      if (!action) {
        return;
      } else if (action === true) {
        // 移除已存在的目录
        console.log(`\r\n移除目录中...`);
        await fs.remove(targetPath);
        // 移除已存在的目
        console.log(`\r\n移除目录成功!`);
      }
    }
  }

  // 创建项目
  const generator = new Generator(name, targetPath);

  // 开始创建项目
  generator.create();
};

/lib/Generator.js

const inquirer = require("inquirer");
const fs = require("fs");
const path = require("path");
const Animate = require("./Animate");
const figlet = require("figlet");

class Generator {
  constructor(name, targetPath) {
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetPath = targetPath;
    // gis目录
    this.gisList = [];
    // 目标package.json路径
    this.packageJsonPath = path.resolve(this.targetPath, "./package.json");
    this.frameTemplate = "";
    this.useRuoyi = false;
    this.useGis = false;
  }

  // 核心创建逻辑
  async create() {
    console.log('\r\n开始构建项目...')
    await this.question();
    this.useAnimate();
    await this.copyTemplate();
    await this.handleGis();
  }

  // 指引提问
  async question() {
    const { frameTemplate, useRuoyi, useGis } = await inquirer.default.prompt([
      {
        type: "list",
        name: "frameTemplate",
        message: "请选择框架",
        choices: ["Vue3", "Vue2"],
      },
      {
        type: "confirm",
        name: "useRuoyi",
        message: "是否使用若依框架",
      },
      {
        type: "confirm",
        name: "useGis",
        message: "是否使用地图组件",
      },
    ]);
    this.frameTemplate = frameTemplate;
    this.useRuoyi = useRuoyi;
    this.useGis = useGis;

    // 使用地图则选择23维
    if (this.useGis) {
      const res = await inquirer.default.prompt([
        {
          type: "checkbox",
          name: "gisList",
          message: "请选择要使用的地图框架",
          choices: ["macrogis", "macroglobe"],
        },
      ]);
      this.gisList = res.gisList;
    }
  }

  // 使用动画
  async useAnimate() {
    // 开始构建项目
    const animate = new Animate();
    animate.start("开始复制模板代码...");
    setTimeout(() => {
      animate.change("开始搭建依赖...");
      setTimeout(() => {
        // 构建结束
        animate.stop();
        animate.message("构建成功!", "succeed");
        console.log(
          "\r\n" +
            figlet.textSync("my-cli", {
              font: "Standard",
              horizontalLayout: "default",
              verticalLayout: "default",
              width: 80,
              whitespaceBreak: true,
            })
        );
        console.log("\r\n如何使用:");
        console.log(`cd ./${this.name}`);
        console.log("npm i");
        console.log("npm run dev");
      }, 3000);
    }, 4000);
  }

  // 复制模板
  async copyTemplate() {
    // 设置原名称
    let templateName = "";
    if (this.useRuoyi && this.frameTemplate == "Vue3") {
      templateName = "ruoyi-vue3";
    } else if (this.useRuoyi && this.frameTemplate == "Vue2") {
      templateName = "ruoyi-vue2";
    } else if (!this.useRuoyi && this.frameTemplate == "Vue3") {
      templateName = "vite-vue3";
    } else if (!this.useRuoyi && this.frameTemplate == "Vue2") {
      templateName = "webpack-vue2";
    }
    // 设置源目录
    const sourcePath = path.resolve(__dirname, `../templates/${templateName}`);
    // 复制目录
    const err = await fs.cpSync(sourcePath, this.targetPath, {
      recursive: true,
    });
    if (err) {
      console.error(err);
      return;
    }
  }

  // 处理gis
  async handleGis() {
    // 如果选择了gis
    if (this.gisList.includes("macrogis")) {
      //写入package.json
      const data = await fs.readFileSync(this.packageJsonPath);
      if (!data) {
        console.error("读取json文件失败!");
        return;
      }
      const jsonData = JSON.parse(data);
      // 固定稳定版本1.3.6
      jsonData.dependencies.macrogis = "1.3.6";
      const err = await fs.writeFileSync(
        this.packageJsonPath,
        JSON.stringify(jsonData, null, 4)
      );
      if (err) {
        console.error("写入json文件失败!");
        return;
      }
      // 复制到node_modules
      const gisPath = path.resolve(__dirname, `../templates/macrogis`);
      const modulePath = path.resolve(
        this.targetPath,
        "./node_modules/macrogis"
      );
      fs.cpSync(gisPath, modulePath, { recursive: true });
    }
    // 如果选择了globe
    if (this.gisList.includes("macroglobe")) {
      //写入package.json
      const data = await fs.readFileSync(this.packageJsonPath);
      if (!data) {
        console.error("读取json文件失败!");
        return;
      }
      const jsonData = JSON.parse(data);
      // 固定稳定版本1.3.4
      jsonData.dependencies.macroglobe = "1.3.4";
      const err = await fs.writeFileSync(
        this.packageJsonPath,
        JSON.stringify(jsonData, null, 4)
      );
      if (err) {
        console.error("写入json文件失败!");
        return;
      }
      // 复制到node_modules
      const globePath = path.resolve(__dirname, `../templates/macroglobe`);
      const modulePath = path.resolve(
        this.targetPath,
        "./node_modules/macroglobe"
      );
      fs.cpSync(globePath, modulePath, { recursive: true });
    }
  }
}

module.exports = Generator;

package.json

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "cli",
  "main": "./bin/cli.js",
  "bin": {
    "my-cli": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "commander": "^14.0.0",
    "ejs": "^3.1.10",
    "figlet": "^1.8.2",
    "fs-extra": "^11.3.0",
    "inquirer": "^12.9.0",
    "ora": "^4.1.1"
  }
}

相关文章

网友评论

      本文标题:动手搭建cli脚手架

      本文链接:https://www.haomeiwen.com/subject/olfdojtx.html