开发一个项目脚手架

最近自己心血来潮,结合之前沉淀的项目模板(react/vue/koa2…)开发了一个项目脚手架(cli) jupiter,刚好团队内缺少这样一个工具,于是也开始在团队内推广使用

Github:https://github.com/GitHubJackson/jupiter

jupiter v1.0

1.0 版本主要支持以下功能:

  • 根据用户选择生成代码模板(react/vue/vite-vue/koa2
  • 统一的编码和项目规范配置(eslint/stylelint/commitlint
  • 预装公用库,包括社区高效的开源库(ahooks、lodash)、团队内部的组件库、工具库等等
  • 支持检测更新

cli 命令

# 新建项目
jupiter init [项目名]
jupiter -v # --version
jupiter -h # --help
# 手动检测cli更新
jupiter upgrade

开发脚手架

准备

  • Node.js 运行环境
  • npm/yarn

新建项目

npm init -y

jupiter 项目结构参考以下:

├─ bin
│  └─ index.js
├─ lib
│  ├─ init.js
│  ├─ download.js
│  └─ update.js
├─ .gitignore
├─ LICENSE
├─ README.md
├─ yarn.lock
└─ package.json

package.json 增加以下字段,npm link 和全局执行包需要指定 bin

"main": "./bin/index.js",
"bin": {
  "jupiter": "./bin/index.js"
}

然后安装相关依赖

yarn add -D chalk commander download fs-extra handlebars inquirer log-symbols ora update-notifier
  • chalk。实现比较好看的日志输出
  • commander。提供用户命令行输入和参数解析的功能
  • inquirer。用户与命令行交互的工具
  • update-notifier。检查更新
  • fs-extra。fs 加强版
  • ora。实现等待动画
  • handlebars。语义化模板
  • log-symbols。提供各种日志级别的彩色符号

获取版本

package.json 中的 version

修改 bin/index.js

#!/usr/bin/env node
const program = require("commander");

program.version(require("../package.json").version, "-v, --version");

program.parse(process.argv);

其中 #!/usr/bin/env node 必加,主要是让系统看到这一行的时候,会沿着对应路径查找 node 并执行。调试阶段时,为了保证 jupiter 指令可用,我们需要在项目下执行 npm link 软链接到全局(不需要指令时用 npm unlink 断开链接),然后打开终端输入

jupiter -v

查看输出是否正确

检查更新

新增 lib/update.js

const updateNotifier = require("update-notifier");
const chalk = require("chalk");
const pkg = require("../package.json");

const notifier = updateNotifier({
  pkg,
  // 设定检查更新周期,默认为 1 天
  // updateCheckInterval: 1000,
});

function updateChk() {
  if (notifier.update) {
    console.log(
      `New version available: ${chalk.cyan(
        notifier.update.latest
      )}, it's recommended that you update before using.`
    );
    notifier.notify();
  } else {
    console.log("No new version is available.");
  }
}

module.exports = updateChk;

update-notifier 检测更新机制是通过 package.json 文件的 name 字段值和 version 字段值来进行校验:它通过 name 字段值从 npm 获取库的最新版本号,然后再跟本地库的 version 字段值进行比对,如果本地库的版本号低于 npm 上最新版本号,则会有相关的更新提示

修改 bin/index.js

const updateChk = require("../lib/update");

// 检查更新
program
  .command("upgrade")
  .description("Check the jupiter version.")
  .action(() => {
    updateChk();
  });

program.parse(process.argv);

终端执行 jupiter upgrade ,本地测试可以将 package.json 的 name 改为 react 看看效果

注意:Chalk v5 已经使用 esm 重构,所以为了支持 commonjs,在该项目中还是要沿用 v4 版本

下载模板

这里通过 download-git-repo 下载 github 上的模板代码

下载模板的操作要能强制覆盖原有文件,主要是两步:

  1. 清空文件夹
  2. 下载文件并解压到文件夹

新增 lib/download.js

const download = require("download-git-repo");
const ora = require("ora");
const chalk = require("chalk");
const fse = require("fs-extra");
const path = require("path");

const tplPath = path.resolve(__dirname, "../template");

const asyncDownload = function (template, tplPath) {
  return new Promise((resolve, reject) => {
    download(template, tplPath, { clone: true }, function (err) {
      if (err) {
        reject(err);
      }
      resolve();
    });
  });
};

async function dlTemplate(answers) {
  // 先清空模板目录
  try {
    await fse.remove(tplPath);
  } catch (err) {
    console.error(err);
    process.exit();
  }
  const dlSpinner = ora(chalk.cyan("Downloading template..."));
  const { name, type } = answers;
  const templateMap = {
    react: "github:GitHubJackson/react-spa-template#main",
    vue: "github:GitHubJackson/vue-spa-template#main",
    "vite-vue": "github:GitHubJackson/vite-vue-template#main",
    koa: "github:GitHubJackson/koa2-template-lite#main",
  };

  dlSpinner.start();
  // 下载模板后解压
  return asyncDownload(templateMap[type], tplPath)
    .then(() => {
      dlSpinner.text = "Download template successful.";
      dlSpinner.succeed();
    })
    .catch((err) => {
      dlSpinner.text = chalk.red(`Download template failed. ${err}`);
      dlSpinner.fail();
      process.exit();
    });
}

module.exports = dlTemplate;

具体使用参考下面的 init 函数

init

这个是 cli 的关键函数,主要流程就是:

  1. 获取用户输入的项目名和选择的模板类型
  2. 根据模板类型下载项目模板至命令路径

新增 lib/init.js

const fse = require("fs-extra");
const ora = require("ora");
const chalk = require("chalk");
const inquirer = require("inquirer");
const symbols = require("log-symbols");
const handlebars = require("handlebars");
const path = require("path");

const dlTemplate = require("./download");
const tplPath = path.resolve(__dirname, "../template");

async function initProject(projectName) {
  try {
    const processPath = process.cwd();
    // 项目完整路径
    const targetPath = `${processPath}/${projectName}`;
    const exists = await fse.pathExists(targetPath);
    if (exists) {
      console.log(symbols.error, chalk.red("The project is exists."));
      return;
    }

    const promptList = [
      {
        type: "list",
        name: "type",
        message: "选择项目模板",
        default: "react",
        choices: ["react", "vue", "vite-vue", "koa"],
      },
    ];

    // 选择模板
    inquirer.prompt(promptList).then(async (answers) => {
      // 根据配置拉取指定项目
      await dlTemplate(answers);
      // 等待复制好模板文件到对应路径去,模板文件在 ./template 下
      try {
        await fse.copy(tplPath, targetPath);
        console.log("copy success");
      } catch (err) {
        console.log(symbols.error, chalk.red(`Copy template failed. ${err}`));
        process.exit();
      }
    });
  } catch (err) {
    console.error(err);
    process.exit();
  }
}

bin/index.js 增加初始化命令

// init 初始化项目
//...
program
  .name("jupiter")
  .usage("<commands> [options]")
  .command("init <project_name>")
  .description("create a new project.")
  .action((project) => {
    initProject(project);
  });
//...

help

查看帮助

program.on("--help", () => {
  console.log(
    `\r\nRun ${chalk.cyan(
      `zr <command> --help`
    )} for detailed usage of given command\r\n`
  );
});

上传 npm

上传和调试 npm 包可以参考 发布 npm 包

使用

这个是个人日常开发常用的脚手架工具,欢迎使用和 comment,本项目会持续迭代

npm i -g @jacksonzhou52017/jupiter
jupiter init myapp

备注

可以到这里查看后续开发计划 https://github.com/GitHubJackson/fe-engineering/issues/4