Skip to main content

脚手架

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 包