preface

Recently, the author has built two engineering shelves based on create-React-app, one for PC Web and the other for mobile H5. Therefore, there are two projects hanging on gitLab. If there is a new business requirement, When you need to do PC or mobile business, you will open the terminal, copy the corresponding gitlab address git clone XXXX, and a project that has been built will be pulled down, and you can enjoy the development.

Such a method is still relatively primitive, and it is inevitable that the clone project will have to be modified in some scenarios. For example, when a mobile project is launched, some businesses need to introduce WX SDK. js, while others do not, and so on. If such introduction requirements are completed before Clone, Then we can really enjoy the development.

How do you do it before Clone? That’s where cli tools come in. How does the author’s CLI tool work 👇

implementation

It may seem like a big deal, but a CLI does just a few things.

Let’s take it step by step.

Gets the command argument + New

Let projectName = program.args[0]; // Return the current working directory of node.js. Let rootName = path.basename(process.cwd()); fs.mkdirSync(projectName); // Create a folder based on the inputCopy the code

The configuration template

Create a new file to hold the template data

// template.json {"cra": {"name": "create react app template 1", "value": "tmp1", "git": "gitlab: XXXXXXXX ", "options": []}, "mini" : {" name ":" Taro small program ", "value" : "mini", "git" : "gitlab: XXXXXXXX", "options" : []}}Copy the code

Select the template type

The common interactive command-line user interface implemented by the Inquirer library is used here

/ function selectTemplate() {return new Promise(resolve, resolve); reject) => { let choices = Object.values(templateConfig).map(item => { return { name: item.name, value: item.value }; }); Let config = {// type: 'checkbox', type: "list", message: "Please select the item type to create ", name: "select", choices: [new inquirer.Separator(" Separator "),...choices]}; inquirer.prompt(config).then(data => { let { select } = data; let { value, git } = templateConfig[select]; resolve({ git, // templateValue: value }); }); }); } let {git} = await selectTemplate();Copy the code

Download the template

The download-git-repo library is used for the core functionality

Function download (target, url) {// Use target = path.join('./download-temp'); return new Promise((resolve,reject) => { download(`direct:${url}`, target, { clone: (err) = > true}, {the if (err) {the console. The log (chalk. Red (template download failed: (" ")); Reject (err)} else {console.log(" template download complete :)"); resolve(target) } }) }) } templateName = await download(rootName, git);Copy the code

Obtaining local Configuration

function getCustomizePrompt(target, fileName) { return new Promise((resolve) => { const filePath = path.join(process.cwd(), target, FileName) if (fs.existssync (filePath)) {console.log(' read template config file ') let file = require(filePath) resolve(file)} else { Console. log(' this file has no configuration file ') resolve([])}})} Let customizePrompt = await getCustomizePrompt(templateName, 'customize_prompt.js')Copy the code

Configuration item Composition

function render(projectRoot, templateName, customizePrompt) { return new Promise(async (resolve, Reject) => {try {let context = {name: projectRoot, // project filename root: projectRoot, // Project file path downloadTemp: TemplateName // template position}; / / get the default configuration const promptArr = configDefault. GetDefaultPrompt (context); // Add template customization promptarr.push (... customizePrompt); let answer = await inquirer.prompt(promptArr); Let generatorParam = {metadata: {... answer }, src: context.downloadTemp, dest: context.root }; // After getting the configuration, pass the composition method await Generator (generatorParam); resolve(); } catch (err) { reject(err); }}); }Copy the code

Ejs synthesis

The core function here is using metalsmith here, which is responsible for traversing the file module and copying the file module to our target directory, while using we can manipulate the transferred file content

const rm = require("rimraf").sync; const Metalsmith = require("metalsmith"); const ejs = require("ejs"); const path = require("path"); const fs = require("fs"); Function generator(config) {// ejS config let {metadata, SRC, dest} = config; if (! SRC) {return promise.reject (new Error(' invalid source: ${SRC} ')); } return new Promise((resolve, resolve) Reject) => {// declare metalsmith instance const metalsmith = metalsmith (process.cwd()).metadata(metadata).clean(false) .source(src) .destination(dest); // Provide a list of file modules that need to be ignored // Files that do not want to be copied can be ignored according to the customized configuration // for example, web that does not need to be used in wechat environment, Wx.d. ts const ignoreFile = path.resolve(process.cwd(), SRC, '.fileignore'); If (fs.existssync (ignoreFile)) {if (fs.existssync (ignoreFile)) {use((files, metalsmith, done) => { const meta = metalsmith.metadata(); // Render the ignore file first, then cut the contents of the ignore file line by line, Const ignores = ejs.render (fs.readfilesync (ignoreFile).tostring (), meta).split("\n").filter(item =>!! item.length); Keys (files). ForEach (fileName => {// hide the ignores (fileName.includes(ignorePattern)) { delete files[fileName]; }}); }); done(); }); } metalsmith .use((files, metalsmith, done) => { const meta = metalsmith.metadata(); Keys (files).foreach (fileName => {try {const t = files[fileName].contents.tostring (); Files [fileName].contents = new buffer. from(ejs.render(t, meta)); } } catch (err) { console.log("fileName------------", fileName); console.log("er -------------", err); }}); done(); }) .build(err => { rm(src); // Delete err from download-temp. reject(err) : resolve(); }); }); }; await render(projectRoot, templateName, customizePrompt);Copy the code

Depend on the installation

Function afterBuild(name) {inquirer. Prompt ({type: "confirm", name: "install", message: }). Then (data => {if (! data.install) { return } const ls = spawn('yarn', [], { cwd: path.resolve(process.cwd(), path.join(".", name)) }); ls.stdout.on('data', (data) => { console.log(`${data}`); }); ls.stderr.on('data', (data) => { console.error(`${data}`); }); Ls.on ('close', (code) => {console.log(' installed '); }); }); } // Build end afterBuild(projectRoot);Copy the code

Configuration commands

Put all of the above code in the index.js file and pass

node index.js
Copy the code

Should be run up (let’s assume it can run ^_^. This is not convenient or maintainable. We can then treat the entire project as an NPM package that can be updated anytime, anywhere.

npm init
Copy the code

Configured in package JSON

"bin": {
    "lemon": "bin/lemon",
    "lemon-init": "bin/lemon-init"
},
Copy the code

The key function of this command is to use commander to implement the above index.js content in lemon-init. As for lemon content, commander will trigger the init command to execute the index script

// lemon #! /usr/bin/env node const program = require('commander') console.log('version', require('.. /package').version) program .version(require('.. /package').version).usage('<command> [project name]').command('init', 'create new project ') // lemon init wxapp.parse (process.argv)Copy the code

The project directory is as follows

. ├ ─ ─ bin │ ├ ─ ─ lemon │ └ ─ ─ lemon - init ├ ─ ─ the gitignore ├ ─ ─ the template. The json ├ ─ ─ package. The json ├ ─ ─ the README, mdCopy the code

We can initialize it directly from the command line when we need to use it

lemon init wxApp
Copy the code

conclusion

The CLI tools are relatively simple, and the rest of the work is to write different EJS template code for different configurations. I was also confused at first, but I looked through the allen-CLI source code, understood the general process, and made bug fixes and feature cuts and improvements. A scaffold tool suitable for your project is in sight.