最近自己心血来潮,结合之前沉淀的项目模板(react/vue/koa2…)开发了一个项目脚手架(cli) 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 上的模板代码
下载模板的操作要能强制覆盖原有文件,主要是两步:
- 清空文件夹
- 下载文件并解压到文件夹
新增 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 的关键函数,主要流程就是:
- 获取用户输入的项目名和选择的模板类型
- 根据模板类型下载项目模板至命令路径
新增 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