前言
在上篇 工欲善其事必先利其器(配置Vue3 + ts项目模板) 中,我们以 Vue3 + ts
为例配置一个简单的项目模板,加上工欲善其事必先利其器(前端代码规范)篇,整个配置还是挺繁琐的,为了方便使用统一规范的模板,这篇我们来学习制作简易的CLI
准备
初始化一个项目,项目结构如下
├─index.js // 主入口
├─package.json
├─lib
处理命令行
自行处理的话可以参照create-vu,这里选择commander命令行工具,可以非常简单的定义想要的指令,交互式命令选择 prompts。
首先是处理 project name
const { program } = require('commander');
const prompts = require('prompts')
program.version(require('./package.json').version)
.option('-n, --name <string>', 'project name')
.action(async function (cmdArg) {
let { name } = cmdArg
if (!name) {
const answer = await prompts([{
type: 'text',
name: 'name',
message: 'Project name:',
initial: 'project'
}
])
name = answer.name
}
}).parse(process.argv);
执行 node index.js
运行测试,可以看到Project name
的交互提示,也可以执行node index.js -n testName
,测试 -n
指令
这样简单的入口就完成了
下载远程模板
接下来我们使用 download-git-repo 下载在上篇配置好的模板vue3-template
- lib/download.js,封装下载函数,ora需要装 5.x 及以下版本,否则需要使用
ESModule
方式使用
const { promisify } = require('util');
const ora = require('ora')
const download = promisify(require('download-git-repo'));
module.exports.clone = async function (repo, desc) {
const process = ora(`download...${repo}\n`);
process.start();
await download(repo, desc);
process.stop();
}
- index.js 改造如下
const { promisify } = require('util');
const ora = require('ora')
const download = promisify(require('download-git-repo'));
module.exports.clone = async function (repo, desc) {
const process = ora(`download...${repo}\n`);
process.start();
await download(repo, desc);
process.stop();
}
执行node index.js -n testName
测试,可以看到把vue3-template下载到 testName
文件夹下
至此,一个简单的克隆模板的 cli
就制作完成了
根据用户选择生成模板
当然也有想要自行选择工具的需要,可以提供集成的工具选择,再根据用户选择生成模板
选择各种规范
以规范为例,在 工欲善其事必先利其器(前端代码规范) 中,我们了解了各种规范的配置流程,那么这里就简单了,只需要收集选择的工具,把相应的配置写入文件即可
实现流程如下:
- 收集用户选择
- 处理用户选择,也就是待生成文件的内容
- 最后根据处理好的数据写入对应的文件
- lib/init.js
const fs = require("fs");
const {
initESLint,
initStylelint,
initPrettier,
initGitHooks,
initESLintFile,
initPrettierFile,
initStylelintFile,
initGitHooksFile,
renderESLint,
renderStylelint,
renderGitHooks,
renderPrettier,
renderPackage,
} = require("../src/index.js");
let projectConfig = {};
async function init(projectName) {
projectConfig = {
files: {
pkg: {
name: projectName,
version: "0.0.0",
description: "",
main: "index.js",
scripts: {
start: "node index.js",
},
license: "ISC",
devDependencies: {
eslint: "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jsdoc": "^39.6.9",
"eslint-plugin-prettier": "^4.2.1",
},
},
},
};
projectConfig.name = projectName
const cwd = process.cwd();
projectConfig.cwd = cwd;
projectConfig.projectPath = `${cwd}\/${projectName}`;
projectConfig.eslint = await initESLint();
projectConfig.stylelint = await initStylelint();
projectConfig.prettier = await initPrettier();
projectConfig.gitHooks = await initGitHooks();
initFiles(projectConfig)
renderTemplate(projectConfig)
}
function initFiles(config) {
initESLintFile(config);
initStylelintFile(config);
initPrettierFile(config);
initGitHooksFile(config);
}
function renderTemplate(config) {
const { projectPath } = config;
if (!fs.existsSync(projectPath)) {
fs.mkdirSync(projectPath);
}
renderESLint(config);
renderStylelint(config);
renderPrettier(config);
renderGitHooks(config);
renderPackage(config);
}
module.exports = {
init
}
流程处理
然后就是每个工具对应的流程处理,因为大体相同,这里就以 Stylelint
为例进行说明
询问要使用的 CSS 拓展语言
const styleList = [
{ title: "scss", value: "scss" },
{ title: "less", value: "less" },
];
async function initStylelint() {
const { css } = await prompts([
{
type: "multiselect",
name: "css",
message: "Stylelint",
choices: styleList,
},
]);
return css.reduce((a, b) => {
a[[b]] = b;
return a;
}, {});
}
提供 less
、scss
选择,可多选
准备需要写入的文件信息
function initScssConfig(config) {
const {
stylelint: { scss },
} = config;
if (scss) {
config.files.stylelintrc.overrides.push({
extends: ["stylelint-config-recommended-scss", "stylelint-config-recess-order"],
files: ["**/*.scss"],
});
devDependencies = {
"stylelint-config-recommended-scss": "^8.0.0",
"stylelint-scss": "^4.3.0",
"stylelint-config-standard-scss": "^6.1.0",
};
Object.assign(config.files.pkg.devDependencies, devDependencies);
}
}
function initLessConfig(config) {
const {
stylelint: { less },
} = config;
if (less) {
config.files.stylelintrc.overrides.push({
extends: ["stylelint-config-recommended-less", "stylelint-config-recess-order"],
customSyntax: "postcss-less",
files: ["**/*.less"],
});
devDependencies = {
less: "^4.1.3",
"less-loader": "^11.0.0",
"postcss-less": "^6.0.0",
"stylelint-less": "^1.0.6",
};
Object.assign(config.files.pkg.devDependencies, devDependencies);
}
}
function initStylelintFile(config) {
config.files.stylelintrc = {
extends: ["stylelint-config-standard", "stylelint-config-recess-order"],
overrides: [],
rules: {
"no-empty-first-line": true,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["deep"],
},
],
},
};
const devDependencies = {
"stylelint-config-recess-order": "^3.1.0",
"stylelint-config-standard": "^29.0.0",
};
Object.assign(config.files.pkg.devDependencies, devDependencies);
initScssConfig(config);
initLessConfig(config);
}
就是根据选择调整 config
配置
写入文件
async function renderStylelint(config) {
const { files, projectPath } = config;
const { stylelintrc } = files;
writeFile(`${projectPath}/.stylelintrc`, stylelintrc);
}
到这一个可选择配置流程基本就完成了,最后是接入主流程中
const { program } = require('commander');
const prompts = require('prompts')
const { downloadTemplate, useTemplate, canUseTemplatesName } = require('./lib/initTemplate')
const { init } = require('./lib/init')
program.version(require('./package.json').version)
.option('-n, --name <string>', 'project name')
.option('-t, --template <string>', 'use template name (support: vue3)')
.action(async function (cmdArg) {
let { name, template } = cmdArg
if (!name) {
const answer = await prompts([{
type: 'text',
name: 'name',
message: 'Project name:',
initial: 'project'
}
])
name = answer.name
}
if (canUseTemplatesName.includes(template)) {
downloadTemplate(template, name)
return
}
const { isUseTemplate } = await prompts([
{
type: "toggle",
name: "isUseTemplate",
message: "use template?",
initial: true,
active: "yes",
inactive: "no",
},
])
if (isUseTemplate) {
useTemplate(name)
return
}
init(name)
// console.log(cmdArg);
}).parse(process.argv);
更详细的代码请参考moloch-create
动态模板
到这里,还有一个问题就是,如果需要选择框架如:vue、react等,对应配套的工具也都是变化,这种情况可以使用类似handlebars的库做动态生成的底层模板,篇幅原因,这个就不详细说明了
测试
package.json
需要配置 一些基本信息,如
{
"name": "m-create",
"version": "0.0.1",
"main": "index.js"
}
然后执行 npm link
命令将包 link
到本地全局,执行 npx m-create
进行测试
发布
- 首先注册
npm
账号 - 登录:
npm login
- 发布:
npm publish
因为每次发布需要改版本,这里可以配个简单的脚本,
- publish.sh
#!/bin/sh
set -e
# 交互式选择发布版本
VERSION=`npx select-version-cli`
# commit
# git add -A
# git commit -am "release: $VERSION"
npm version $VERSION --message "[release] $VERSION"
echo "push... v$VERSION"
# push
git push
# push tag
git push origin v$VERSION
# publish
npm publish
echo "✅ Publish completed"
package.json
增加 scripts
命令:
"scripts": {
"pub": "pnpm publish.sh"
},
每次提交完代码,如果需要发布则执行 npm run pub
发布脚本,会提示版本选择并根据更改版本并自动提交代码,发布前会打上版本 tag
如果 sh
执行不了,可以选择Git Base
或者可以配置系统变量: 系统属性 > 环境变量 > 系统变量 > Path
注 如果还没熟悉整套流程的,尝试过程可能会有各种问题,也可以本地搭建私仓来做发布测试,私仓搭建可以参照 Docker 部署npm私仓 Verdaccio
最后
本篇讲解了一个简单的Cli
简易实现,对应Cli
已发布至 npm: m-create,可以去尝试看看,因为是未打包发布,所以会提示安装一些依赖,选择 y
即可
附录
制作CLI
的相关工具有很多,这里简单列一些仅供参考
原文链接:https://juejin.cn/post/7214318587431059514 作者:摩洛克