foreword
More than a quarter of 2022 has passed, and it seems that one article per month as promised before has not been fulfilled. Recently, the company is doing some front-end engineering related things. Although the preparation of the component library has been killed by the leaders, but before that, I wrote a scaffolding tool. After all, in this environment, scaffolding tools are flooded, so of course, it is necessary to Write and play.
final effect
Support function
- Choose the web or mobile terminal independently;
- Choose the project framework react or vue independently;
- Independently choose whether to set the remote git address;
- Support for custom variable substitution in project templates;
- Choose whether to install dependencies automatically, you can choose npm, cnpm, yarn;
- Support online update using update command;
- Independently choose whether to overwrite the existing file directory;
develop
Initialize the project
Then start the development. First, let's create a new project folder called new-cli
, create a new package.json
file in the project folder, and set the commonly used fields. as follows:
{
"name": "new-cli",
"version": "1.0.0",
"description": "a react project cli, help you create a react project quickly",
"bin": {
"new-cli": "bin/www.js"
},
"dependencies": {
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"commander": "^9.1.0",
"consolidate": "^0.16.0",
"cross-spawn": "^7.0.3",
"download-git-repo": "^3.0.2",
"ejs": "^3.1.6",
"fs-extra": "^10.0.1",
"inquirer": "^8.2.1",
"metalsmith": "^2.4.2",
"ora": "^5.4.1",
"figlet": "^1.5.2",
"semver": "^7.3.5",
"shelljs": "^0.8.5"
},
"repository": {
"type": "git",
"url": "https://github.com/BoWang816/new-cli.git"
},
"keywords": [
"cli",
"react"
],
"author": "恪晨",
"publishConfig": {
"registry": "私有仓库地址"
},
"engines": {
"node":"^12.20.0 || >=14"
}
}
After passing the above settings, the name of our scaffold is called new-cli, which means that the installation will be done through npm install -g new-cli
. The name set under bin is to set the command executed by the scaffold, and it is used as the entry file from the bin/www.js file; dependencies
is the project dependency we need, it is worth noting that like boxen, chalk 、Dependency packages such as figlet have not been introduced in the latest version, so we need to install the lower version of the package; publishConfig
can set the npm address that needs to be released at that time, if you build If you have an npm private server, you can publish it to your private server by setting the registry.
Set project entry
After it is built package.json
we will start to build the entry file, that is, www.js under bin. In fact, your entry file can also be placed in the root directory, according to your own preferences, of course, if you place it in the If the root directory is set, the following bin should be changed to new-cli: './www.js'
. Www.js mainly introduces toolkits such as commander and inquirer to initialize scaffolding tools. Because www.js is going to run as a node script, the environment needs to be declared at the top: #! /usr/bin/env node
, the scaffold I wrote involves the three commands init, update, help, and help is commander It is supported by itself, and here is just a little customization.
Initialize init command, update command, help command
First, we need to introduce commander and use its program,
const {program} = require("commander");
, the main body of the scaffolding tool is it, we initialize the relevant commands:#! /usr/bin/env node // 引入commander const {program} = require("commander"); // 初始化init命令, project-name就是你的项目名称与项目文件夹名称 program.command("init <project-name>") // init命令描述 .description("create a new project name is <project-name>") // init命令参数项,因为后续会设置支持覆盖文件夹,所以这里提供一个-f参数 .option("-f, --force", "overwrite target directory if it exists") // init命名执行后做的事情 .action(() => { console.log('doSomething'); }); program.command("update") .description("update the cli to latest version") // update命令执行后做的事情,自动检测更新 .action(async () => { // await checkUpdate(); console.log('update'); }); program.on("--help", () => { // 监听--help命令,输出一个提示 console.log(figlet.textSync("new-cli", { font: "Standard", horizontalLayout: 'full', verticalLayout: 'fitted', width: 120, whitespaceBreak: true })); }); // 这个一定不能忘,且必须在最后!!! program.parse(process.argv);
By setting the above, we can actually use the basic commands. There are two ways to debug locally. One is to directly link the scaffolding tool we wrote to the local global npm through the npm link
command, and the other is to directly execute it through node bin/www.js
This js file, here we can use the latter.
Extended init command
Next, we need to extend the init naming, which is to do something in the action. First of all, we provide the parameter option of -f, the purpose is to overwrite if a folder with the same name is detected when the project is initialized, so in the first step of initializing the project, we need to detect whether there is a folder with the same name in the current path. , and a prompt message is given when -f is not set, and a second prompt is given after -f is set. If you agree to overwrite, the project will be initialized. Therefore, the following content will be executed in the action function. Here we need to introduce chalk, paht, fs-extray and the create that we wrote ourselves.
const chalk = require("chalk");
const path = require("path");
const fs = require('fs-extra');
const figlet = require('figlet');
const create = require('../utils/create');
program
.command("init <project-name>")
.description("create a new project name is <project-name>")
.option("-f, --force", "overwrite target directory if it exists")
.action(async (projectName, options) => {
const cwd = process.cwd();
// 拼接到目标文件夹
const targetDirectory = path.join(cwd, projectName);
// 如果目标文件夹已存在
if (fs.existsSync(targetDirectory)) {
if (!options.force) {
// 如果没有设置-f则提示,并退出
console.error(chalk.red(`Project already exist! Please change your project name or use ${chalk.greenBright(`new-cli create ${projectName} -f`)} to create`))
return;
}
// 如果设置了-f则二次询问是否覆盖原文件夹
const {isOverWrite} = await inquirer.prompt([{
name: "isOverWrite",
type: "confirm",
message: "Target directory already exists, Would you like to overwrite it?",
choices: [
{name: "Yes", value: true},
{name: "No", value: false}
]
}]);
// 如需覆盖则开始执行删除原文件夹的操作
if (isOverWrite) {
const spinner = ora(chalk.blackBright('The project is Deleting, wait a moment...'));
spinner.start();
await fs.removeSync(targetDirectory);
spinner.succeed();
console.info(chalk.green("✨ Deleted Successfully, start init project..."));
console.log();
// 删除成功后,开始初始化项目
// await create(projectName);
console.log('init project overwrite');
return;
}
console.error(chalk.green("You cancel to create project"));
return;
}
// 如果当前路径中不存在同名文件夹,则直接初始化项目
// await create(projectName);
console.log('init project');
});
Let's look at the current effect again:
Create the create method
In the previous operation, after overwriting the file with the same name, we used the
await create(projectName)
method to initialize the project, and then we started to develop the create method. Create a new folder in the root directory calledutils
, of course you can call it lib or✨点赞
, and create a new file under utils calledcreate.js
, in this file, we will set the execution of some questions asked in the download initialization project. The main contents are as follows:const inquirer = require("inquirer"); const chalk = require("chalk"); const path = require("path"); const fs = require("fs"); const boxen = require("boxen"); const renderTemplate = require("./renderTemplate"); const downloadTemplate = require('./download'); const install = require('./install'); const setRegistry = require('./setRegistry'); const {baseUrl, promptList} = require('./constants'); const go = (downloadPath, projectRoot) => { return downloadTemplate(downloadPath, projectRoot).then(target => { //下载模版 return { downloadTemp: target } }) } module.exports = async function create(projectName) { // 校验项目名称合法性,项目名称仅支持字符串、数字,因为后续这个名称会用到项目中的package.json以及其他很多地方,所以不能存在特殊字符 const pattern = /^[a-zA-Z0-9]*$/; if (!pattern.test(projectName.trim())) { console.log(`\n${chalk.redBright('You need to provide a projectName, and projectName type must be string or number!\n')}`); return; } // 询问 inquirer.prompt(promptList).then(async answers => { // 目标文件夹 const destDir = path.join(process.cwd(), projectName); // 下载地址 const downloadPath = `direct:${baseUrl}/${answers.type}-${answers.frame}-template.git#master` // 创建文件夹 fs.mkdir(destDir, {recursive: true}, (err) => { if (err) throw err; }); console.log(`\nYou select project template url is ${downloadPath} \n`); // 开始下载 const data = await go(downloadPath, destDir); // 开始渲染 await renderTemplate(data.downloadTemp, projectName); // 是否需要自动安装依赖,默认否 const {isInstall, installTool} = await inquirer.prompt([ { name: "isInstall", type: "confirm", default: "No", message: "Would you like to help you install dependencies?", choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] }, // 选择了安装依赖,则使用哪一个包管理工具 { name: "installTool", type: "list", default: "npm", message: 'Which package manager you want to use for the project?', choices: ["npm", "cnpm", "yarn"], when: function (answers) { return answers.isInstall; } } ]); // 开始安装依赖 if (isInstall) { await install({projectName, installTool}); } // 是否设置了仓库地址 if (answers.setRegistry) { setRegistry(projectName, answers.gitRemote); } // 项目下载成功 downloadSuccessfully(projectName); }); }
In the
create.js
file, we first determine whether the initialized project name contains special characters, if it does, an error message will be given, and the project initialization will be terminated. If the project name is legal, start asking the user for the project template they want:
We extract the list of these queries as constants, and also extract the address of the template as constants, so we need to create aconstants.js
file in the utils folder, the contents of which are as follows:/** * constants.js * @author kechen * @since 2022/3/25 */ const { version } = require('../package.json'); const baseUrl = 'https://github.com/BoWangBlog'; const promptList = [ { name: 'type', message: 'Which build tool to use for the project?', type: 'list', default: 'webpack', choices: ['webpack', 'vite'], }, { name: 'frame', message: 'Which framework to use for the project?', type: 'list', default: 'react', choices: ['react', 'vue'], }, { name: 'setRegistry', message: "Would you like to help you set registry remote?", type: 'confirm', default: false, choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] }, { name: 'gitRemote', message: 'Input git registry for the project: ', type: 'input', when: (answers) => { return answers.setRegistry; }, validate: function (input) { const done = this.async(); setTimeout(function () { // 校验是否为空,是否是字符串 if (!input.trim()) { done('You should provide a git remote url'); return; } const pattern = /^(http(s)?:\/\/([^\/]+?\/){2}|git@[^:]+:[^\/]+?\/).*?.git$/; if (!pattern.test(input.trim())) { done( 'The git remote url is validate', ); return; } done(null, true); }, 500); }, } ]; module.exports = { version, baseUrl, promptList }
Among them, version is our scaffolding version number, baseUrl is the base address of the project template download, promptList is the list of questions to ask the user, and the specific writing of promptList is based on the
inquirer.prompt()
method. The official document address will be attached, and you can play by yourself.- After obtaining the results of user feedback through
inquirer.prompt()
, we will get the relevant field values, and then splicing out the downloaded project template address, the next step is to start downloading the project template. Here we wrote two functions, go function and renderTemplate , one for downloading the project template and one for rendering the project template (because it involves variable substitution). The go function actually uses the downloadTemplate method introduced from the outside, so we need to pay attention to the downloadTemplate and renderTemplate methods, which is the focus of the next chapter.
Create download method
In the
utils
folder, create a new file nameddownload.js
, the content of the file is as follows:/** * 下载 * download.js * @author kechen * @since 2022/3/25 */ const download = require('download-git-repo') const path = require("path") const ora = require('ora') const chalk = require("chalk"); const fs = require("fs-extra"); module.exports = function (downloadPath, target) { target = path.join(target); return new Promise(function (resolve, reject) { const spinner = ora(chalk.greenBright('Downloading template, wait a moment...\r\n')); spinner.start(); download(downloadPath, target, {clone: true}, async function (err) { if (err) { spinner.fail(); reject(err); console.error(chalk.red(`${err}download template failed, please check your network connection and try again`)); await fs.removeSync(target); process.exit(1); } else { spinner.succeed(chalk.greenBright('✨ Download template successfully, start to config it: \n')); resolve(target); } }) }) }
In this file, we use
download-git-repo
this third-party tool library for downloading project templates, because the return result of download-git-repo is the download success or failure, when we use the asynchronous method If it is used directly, there will be problems, so it is encapsulated as a promise here. When err, an exception is thrown to the user. If successful, the target folder path will be returned for subsequent use. Increate.js
we use the go function, after the go function is successfully executed, it will return a data, which gets the path of the project to be downloaded to the specific folder, in fact, mainly to get the promise in the download The resolve result, after getting the path of the target folder, in fact, the project template has been downloaded to the folder, you can start the renderTemplate.- Create renderTemplate method
In the utils
folder, create a new file called renderTemplate.js
, the main purpose of this function is to replace the variables set in the initialized project, mainly using metalSmith
consolidate
these two third-party packages, by traversing the files in the initialization project, convert them to ejs templates, and replace the relevant variables. This method refers to the method of vww-cli . By reading the ask.ts
file in the project template, the customized query list in the project template is obtained, and then the file template engine is rendered to replace the relevant set variables. ,The main contents are as follows:
/**
* 渲染模板
* renderTemplate.js
* @author kechen
* @since 2022/3/24
*/
const MetalSmith = require('metalsmith');
const {render} = require('consolidate').ejs;
const {promisify} = require('util');
const path = require("path");
const inquirer = require('inquirer');
const renderPro = promisify(render);
const fs = require('fs-extra');
module.exports = async function renderTemplate(result, projectName) {
if (!result) {
return Promise.reject(new Error(`无效的目录:${result}`))
}
await new Promise((resolve, reject) => {
MetalSmith(__dirname)
.clean(false)
.source(result)
.destination(path.resolve(projectName))
.use(async (files, metal, done) => {
const a = require(path.join(result, 'ask.ts'));
// 读取ask.ts文件中设置好的询问列表
let r = await inquirer.prompt(a);
Object.keys(r).forEach(key => {
// 将输入内容前后空格清除,不然安装依赖时package.json读取会报错
r[key] = r[key]?.trim() || '';
})
const m = metal.metadata();
const tmp = {
...r,
// 将使用到的name全部转换为小写字母
name: projectName.trim().toLocaleLowerCase()
}
Object.assign(m, tmp);
// 完成后删除模板中的文件
if (files['ask.ts']) {
delete files['ask.ts'];
await fs.removeSync(result);
}
done()
})
.use((files, metal, done) => {
const meta = metal.metadata();
// 需要替换的文件的后缀名集合
const fileTypeList = ['.ts', '.json', '.conf', '.xml', 'Dockerfile', '.json'];
Object.keys(files).forEach(async (file) => {
let c = files[file].contents.toString();
// 找到项目模板中设置好的变量进行替换
for (const type of fileTypeList) {
if (file.includes(type) && c.includes('<%')) {
c = await renderPro(c, meta);
files[file].contents = Buffer.from(c);
}
}
});
done()
})
.build((err) => {
err ? reject(err) : resolve({resolve, projectName});
})
});
};
Through the renderTemplate method, we have basically completed the main functions of our scaffolding. We can create a project using the init command. I encountered a problem here, that is, when deleting the ask.ts file, if you don't add await fs.removeSync(result);
this file can't be deleted, but it is unreasonable to add it, the specific reason is not found, do you know Friends can leave a message to explain, thank you very much. At this point, the function of our initialization project has been completed, and the next step is to expand.
Create setRegistry method
In the
utils
folder, create a new file calledsetRegistry.js
, mainly to help users initialize the git address of the project, in the user creation is to choose whether to automatically set the project warehouse address, if set If the project address is set, git will be automatically initialized here, and the project address will be set. The details are as follows:/** * 设置仓库地址 * setRegistry.js * @author kechen * @since 2022/3/28 */ const shell = require("shelljs"); const chalk = require("chalk"); module.exports = function setRegistry(projectName, gitRemote) { shell.cd(projectName); if (shell.exec('git init').code === 0) { if (shell.exec(`git remote add origin ${gitRemote}`).code === 0) { console.log(chalk.green(`✨ \n Set registry Successfully, now your local gitRemote is ${gitRemote} \n`)); return; } console.log(chalk.red('Failed to set.')); shell.exit(1); } };
Create an install method
In the
utils
folder, create a new file calledinstall.js
, mainly to help users install dependencies automatically, the main content is as follows:/** * 安装依赖 * install.js * @author kechen * @since 2022/3/22 */ const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.projectName || process.cwd(); return new Promise((resolve, reject) => { const command = options.installTool; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, {cwd, stdio: ["pipe", process.stdout, process.stderr]}); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); };
Create the checkUpdate method
Under the
utils
folder, create a new file calledcheckUpdate.js
, mainly to help users automatically detect and update the scaffolding, the main content is as follows:/** * 检查更新 * checkUpdate.js * @author kechen * @since 2022/3/23 */ const pkg = require('../package.json'); const shell = require('shelljs'); const semver = require('semver'); const chalk = require('chalk'); const inquirer = require("inquirer"); const ora = require("ora"); const updateNewVersion = (remoteVersionStr) => { const spinner = ora(chalk.blackBright('The cli is updating, wait a moment...')); spinner.start(); const shellScript = shell.exec("npm -g install new-cli"); if (!shellScript.code) { spinner.succeed(chalk.green(`Update Successfully, now your local version is latestVersion: ${remoteVersionStr}`)); return; } spinner.stop(); console.log(chalk.red('\n\r Failed to install the cli latest version, Please check your network or vpn')); }; module.exports = async function checkUpdate() { const localVersion = pkg.version; const pkgName = pkg.name; const remoteVersionStr = shell.exec( `npm info ${pkgName}@latest version`, { silent: true, } ).stdout; if (!remoteVersionStr) { console.log(chalk.red('Failed to get the cli version, Please check your network')); process.exit(1); } const remoteVersion = semver.clean(remoteVersionStr, null); if (remoteVersion !== localVersion) { // 检测本地安装版本是否是最新版本,如果不是则询问是否自动更新 console.log(`Latest version is ${chalk.greenBright(remoteVersion)}, Local version is ${chalk.blackBright(localVersion)} \n\r`) const {isUpdate} = await inquirer.prompt([ { name: "isUpdate", type: "confirm", message: "Would you like to update it?", choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] } ]); if (isUpdate) { updateNewVersion(remoteVersionStr); } else { console.log(); console.log(`Ok, you can run ${chalk.greenBright('wb-cli update')} command to update latest version in the feature`); } return; } console.info(chalk.green("Great! Your local version is latest!")); };
It should be noted here that because the scaffolding is installed globally and involves permissions, it needs to be updated with
sudo new-cli update
under mac, while in windows, you need to open the command line tool as an administratornew-cli update
execute-new-cli update
to update. At this point, our scaffolding is basically complete.
Other bells and whistles
The main functions are basically the above. In addition, we need to add a prompt after the project is successfully created. There is a downloadSuccessfully method at the end of the above create.js
, which is actually a prompt after the successful creation. The main content as follows:
const downloadSuccessfully = (projectName) => {
const END_MSG = `${chalk.blue("🎉 created project " + chalk.greenBright(projectName) + " Successfully")}\n\n 🙏 Thanks for using wb-cli !`;
const BOXEN_CONFIG = {
padding: 1,
margin: {top: 1, bottom: 1},
borderColor: 'cyan',
align: 'center',
borderStyle: 'double',
title: '🚀 Congratulations',
titleAlignment: 'center'
}
const showEndMessage = () => process.stdout.write(boxen(END_MSG, BOXEN_CONFIG))
showEndMessage();
console.log('👉 Get started with the following commands:');
console.log(`\n\r\r cd ${chalk.cyan(projectName)}`);
console.log("\r\r npm install");
console.log("\r\r npm run start \r\n");
}
The specific implementation effect is like this. Here I cut the picture I made before.
project template
We need to create a project template, which needs to contain a ask.ts
file in the root directory, and the others are the same as normal projects. An example of the file content of aks.ts is as follows,
/**
* demo
* aks.ts
* @author kechen
* @since 2022/3/24
*/
module.exports = [
{
name: 'description',
message: 'Please enter project description:',
},
{
name: 'author',
message: 'Please enter project author:',
},
{
name: 'apiPrefix',
message: 'Please enter project apiPrefix:',
default: 'api/1.0',
// @ts-ignore
validate: function (input) {
const done = this.async();
setTimeout(function () {
// 校验是否为空,是否是字符串
if (!input.trim()) {
done(
'You can provide a apiPrefix, or not it will be default【api/1.0】',
);
return;
}
const pattern = /[a-zA-Z0-9]$/;
if (!pattern.test(input.trim())) {
done(
'The apiPrefix is must end with letter or number, like default 【api/1.0】',
);
return;
}
done(null, true);
}, 300);
},
},
{
name: 'proxy',
message: 'Please enter project proxy:',
default: 'https://www.test.com',
// @ts-ignore
validate: function (input) {
const done = this.async();
setTimeout(function () {
// 校验是否为空,是否是字符串
if (!input.trim()) {
done(
'You can provide a proxy, or not it will be default【https://www.test.com】',
);
return;
}
const pattern =
/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/;
if (!pattern.test(input.trim())) {
done(
'The proxy is must end with letter or number, like default 【https://www.test.com】',
);
return;
}
done(null, true);
}, 300);
},
},
];
Here I set four variables: description, author, apiPrefix, proxy. When using it, you only need to pass <%= var %>
in this way, var can be any variable you set in ask.ts , the specific use of the demo is as follows, of course, the file type to be replaced must be the file with the suffix name set in the renderTemplate function mentioned above. In this way, you can freely add variables to the project template without needing to update the scaffolding tools.
{
"name": "xasrd-fe-mobile",
"description": "<%= description %>",
"private": true,
"author": "<%= author %>"
}
At this point, our scaffolding is all developed, and the next step is how to publish it to npm or npm private server.
release
As we mentioned above, if you need to publish the npm private server, you need to configure publishConfig in package.json
and point to the address of the npm private server. When publishing, you need to use the following command to publish:
Private server npm release
- Login to private server
npm login --registry=http://xxxxx
xxxxx is your private server address - release
npm publish
- Login to private server
Official npm release
- Direct
npm login
, thennpm publish
- The premise is that your npm source points to the official npm
- Direct
Automatically trigger npm releases via github actions
- For details, please refer to: Record in detail the development of an npm package that encapsulates commonly used front-end tool functions
Of course, it should be noted that when publishing, the version number in package.json cannot be repeated! ! !
Summarize
At this point, we have completely developed a relatively simple front-end scaffolding tool, which can be released and used. In fact, the specific method is not difficult. There are many third-party toolkits that can be used. Of course, because the interaction of this tool is relatively simple, you can also expand your own ideas and do some more fancy functions. The demo of the example will not be released. Basically all the content is mentioned above, and everyone can play freely. Of course, based on this set, I also wrote an address at https://www.npmjs.com/package/wb-fe-cli , but because there is really no time recently, so the project template is not yet available, and it is not yet able to run completely. , it will be updated gradually.
refer to
- inquirer
- boxen
- vee-cli
- Record in detail the development of an npm package that encapsulates commonly used front-end tool functions
Epilogue
Finally, I hope that after reading this article, it will be helpful to you. If you are diligent, you can write it by yourself. In addition, I hope everyone can pay attention to my Github , hahahaha, I will show you the snake!
You can also pay attention to GridManager , an easy-to-use form plug-in, which supports React and Vue, and is very easy to use!
In the next issue, I will introduce you to some of my commonly used Mac software, which can help you greatly improve your work efficiency in daily development and work! ! ! You can preview Kechen's Mac software recommendations first.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。