preface

At present, the front end is developing day by day, and the engineering is becoming more and more mature. A lot of great frameworks and tools have emerged during this time. At the same time, with the use of scaffolding with the frame is also coming. The development of front-end scaffolding tools, such as VUE-CLI, create-React-app, etc., are commonly used in vue and React development projects. Xiaobian in the vuE-CLI3, VUE-CLI2 scaffolding implementation, whim to achieve a simple version of their own scaffolding, below we come to learn about the scaffolding implementation process.

Implementation approach

I think the implementation differences between VUE-CLI3 and VUe-cli2 are as follows

  • isvue-cli3Not fromgitThe repository downloads the templates, instead generating the code itself and creating files and folders.
  • vue-cli3thewebpackThe configuration is built in, not exposed, and provides user-defined configuration files from defining your own configuration; whilevue-cli2The configuration is completely exposed and can be modified at will.

This article is based on vuE-CLI2 implementation ideas to implement a simple react version scaffolding step by step. Below is a small series of overall implementation process

2. Use the COMMANDER tool to add parsing parameters to your LBS command, parse parameters, and add custom commands. 3. Use the Inquirer command line to interact with the user (user input, selection). 5. Modify the files in the template (package.json, index.html, etc.). 6

Start coding

This article implements a LBS init [projectName] –force command

ProjectName: the entered projectName –force: the defined option (whether to force overwrite if the entered [projectName] folder exists in the current directory)

Add Scaffolding command (LBS)

In package.json, we can add the following code

"bin":{
  "lbs": "./bin/lbs.js"
},
Copy the code

Then create the bin/lbs.js file and add the test code:

#! /usr/bin/env node console.log("hello lbs-cli")Copy the code

The first line, which must be added, specifies that node is used here to parse the script. The default directory is /usr/bin. If the directory cannot be found, go to the system environment variable.

Then we open CMD window in any directory, type LBS command, you will find no command. In fact, we still need to do a step, is to install the local project globally, under the current project run NPM install. -g, and then run the LBS command under CMD, you will find the output of the string we printed.

Now that we’ve added our own LBS commands to the system, how do we add init, create, –version, and so on to LBS?

Use Commander to enrich our LBS command

For those unfamiliar with the use of Commander, see the Commander documentation

We’ll start by installing the plug-in, and then take a first stab at adding version viewing options to our LBS command

const { program } = require("commander") const pkg = require("./.. /package.json") program.version(pkg.version,'-v --version') program.parse(process.argv)Copy the code

If you run LBS -v or LBS –version on any command line, you can see the version output on the console

Next add a command to the LBS command:

Program.mand ('init [projectName]').description(" Initializes the project ") // Add an option.option ('-f --force',' force to delete the project directory if there is an input project directory '). Action ((projectName, CMD)=>{// projectName is the argument we entered, Console. log(projectName) // CMD is the Command object console.log(cmd.force)})Copy the code

Here we have added an init command that supports an optional argument and an optional -f option so let’s do this

lbs init test -f
Copy the code

The test and CMD objects we entered can be viewed on the console. The force attribute can be found in CMD.

If LBS init is executed, the output is as follows

If LBS init test is executed, the following output is displayed

We’ll focus on these two data points here, but you can extend other parameters and options if your command has other complex functions.

Here are justcommandA way to use when we arecommandAdding a second description argument means using a separate executable as a subcommand, such as your command isinitThen you need to create onelbs-initScript file, this file is responsible for executing the command you specify, according tolbs-${command}The way we created the scriptlbs-init.jsfile

Modify the command as follows, adding a second parameter to the command method

Program.mand ('init [projectName]','init project').description(" Initialize the project ") // Add an option.option ('-f --force',' force to delete project directory if entered ').action((projectName, CMD)=>{console.log(projectName) // projectName is the argument we entered, Console. log(cmd.force) // CMD is the Command object})Copy the code

Execute LBS init and you will find nothing output. Because the action method will not be executed, the empty lbS-init. js file we created will be executed. So it doesn’t output anything. Lbs.js only needs to define the init command. Program.mand (‘init [projectName]’,’init project’)

Then add the parsing code to lbS-init.js

const { program } = require("commander") let projectName; let force; Description (" initializes the project ").option('-f --force',' If there is an input project directory, ').action((name, CMD)=>{projectName = name; force = cmd.force; }); program.parse(process.argv); console.log(projectName,force)Copy the code

Re-run LBS init test -f to obtain the data. At this point, we can customize the parameters and options for our LBS init command. What if the user just executes the LBS init command and we don’t get the project name? Please look down

useinquirerImplementation of command line and user interaction (user input, selection, q&A)

The inquirer plugin Chalk is basically a custom color console output

Create a logger.js utility class that prints console information

const chalk = require('chalk');

exports.warn = function(message){
    console.log(chalk.yellow(message));
}

exports.error = function(message){
    console.log(chalk.red(message))
}

exports.info = function(message){
    console.log(chalk.white(message))
}

exports.infoGreen = function(message){
    console.log(chalk.green(message))
}

exports.exit = function(error){
    if(error && error instanceof Error){
        console.log(chalk.red(error.message))
    }
    process.exit(-1);
}
Copy the code

This library is a tool that we can use to interact with users; The first problem is to input the project name. The second problem is to ask the user to select a template. The template needs to be ready on Github. This template is based on the react-apps-template project to rebuild a Git repository. The react+ WebPack4 template does not exist

Const questions = [{type: 'input', name:'projectName', message: Yellow (" Enter your project name: ")}, {type:'list', name:'template', message: Yellow (" Please select Create Project template: "), choices:[ {name:"lb-react-apps-template",value:"lb-react-apps-template"}, {name:"template2",value:"tempalte2"}, {name:"template3",value:"tempalte3"} ] } ]; If (projectName){questions.splice(0,1); if(projectName){questions.splice(0,1); } // Execute user interaction command inquirer.prompt(questions). Then (result=>{if(result.projectname) {projectName = result.projectname; } const templateName = result.template; Log (" projectName: "+ projectName) console.log(" templateName:" + templateName) if(! templateName || ! ProjectName){// exit logger.exit(); } // go down checkProjectExits(projectName,templateName); // Check if the directory exists}). Catch (error=>{logger.exit(error); })Copy the code

Here,checkProjectExitsI’m going to implement it, so I can ignore it. At this point we executelbs init, you can see that the successful acquisitionprojectNameandtemplateName

Next, we need to determine whether the project name entered by the user exists in the current directory. In this case, 1. If the command executed by the user contains –force, then delete the existing directory directly; 2. If the user wants to overwrite, delete the existing folder, but if the user doesn’t allow it, exit

Add the checkProjectExits method to check for the existence of a directory as follows

function checkProjectExits(projectName,templateName){ const currentPath = process.cwd(); const filePath = path.join(currentPath,`${projectName}`); If (force){// Force delete if(fs.existssync (filePath)){// Delete spinner. LogWithSpinner (' delete ${projectName}... `) deletePath(filePath) spinner.stopSpinner(false); } startDownloadTemplate(projectName, templateName) return; } if(fs.existssync (filePath)){// Check whether the file exists and ask whether to continue inquirer. Prompt ({type: 'confirm', name: 'out', message: The '${projectName} folder already exists. Overwrite it? ` }).then(data=>{ if(! Data.out){// the user disagrees with exit(); }else{// delete spinner. LogWithSpinner (' delete ${projectName}... `) deletePath(filePath) spinner.stopSpinner(false); StartDownloadTemplate (projectName, templateName) // Start downloading template}}). Catch (error=>{exit(error); }) }else{ startDownloadTemplate(projectName, }} function startDownloadTemplate(projectName,templateName){ console.log(projectName,templateName) }Copy the code

Here we use a spinner tool class, create lib/spinner. Js, mainly a switch chrysanthemum animation prompt, the code is as follows

const ora = require('ora') const chalk = require('chalk') const spinner = ora() let lastMsg = null exports.logWithSpinner = (symbol, msg) => { if (! Mark) {mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark = mark mark lastMsg.text }) } spinner.text = ' ' + msg lastMsg = { symbol: symbol + ' ', text: msg } spinner.start() } exports.stopSpinner = (persist) => { if (! spinner.isSpinning) { return } if (lastMsg && persist ! == false) { spinner.stopAndPersist({ symbol: lastMsg.symbol, text: lastMsg.text }) } else { spinner.stop() } lastMsg = null }Copy the code

Create lib/ IO. Js and implement deletePath to delete a directory as follows

function deletePath (filePath){ if(fs.existsSync(filePath)){ const files = fs.readdirSync(filePath); for(let index=0; index<files.length; index++){ const fileNmae = files[index]; const currentPath = path.join(filePath,fileNmae); if(fs.statSync(currentPath).isDirectory()){ deletePath(currentPath) }else{ fs.unlinkSync(currentPath); } } fs.rmdirSync(filePath); }}Copy the code

LBS init my-app -f and LBS init -f to check whether my-app is deleted.

Execute LBS init and enter the existing directory name as the project name according to step by step instructions. Select the template and check if the my-app folder is deleted as follows

Download and unzip the template

To download the template, we need to concatenate the URL of the github repository corresponding to the zip package according to the name of the selected template, and then execute the node download code (note that the downloaded zip package is downloaded to the temporary directory of the system). After downloading the zip package successfully, unzip it to the directory where the user entered the project name. After the package is decompressed, delete the downloaded package. That’s the end of the process

Request plug-in was downloaded and decompressed. Decompress plug-in was used. These two plug-ins need to be installed in advance

Override the startDownloadTemplate method above

Function startDownloadTemplate(projectName,templateName){// Start downloading template (projectName,templateName, (error)=>{ if(error){ logger.exit(error); return; } // Replace the extracted template package.json, ReplaceFileContent (projectName,templateName)})} function replaceFileContent(projectName,templateName){ console.log(projectName,templateName); }Copy the code

Create lib/download.js to download template. The code is as follows

const request = require("request") const fs = require("fs") const path = require("path") const currentPath = process.cwd(); const spinner = require("./spinner") const os = require("os") const { deletePath , unzipFile } = require("./io") exports.downloadTemplate = function (templateName,projectName,callBack){ // According to the templateName joining together making the corresponding package const url = {templateName} ` https://github.com/liuboshuo/$url/archive/master. Zip `; Const tempProjectPath = fs.mkdtempsync (path.join(os.tmpdir(), '${projectName}-')); Const file = path.join(tempProjectPath, '${templateName}.zip'); If (fs.existssync (file)){fs.unlinksync (file); } spinner. LogWithSpinner (" Download from template...") ) let stream = fs.createWriteStream(file); request(url,).pipe(stream).on("close",function(err){ spinner.stopSpinner(false) if(err){ callBack(err); return; Const destPath = path.join(currentPath, '${projectName}'); UnzipFile (file,destPath,(error)=>{deletePath(tempProjectPath); unzipFile(file,destPath,(error)=>{deletePath(tempProjectPath); callBack(error); }); })}Copy the code

Add the following method to decompress the zip package in lib/io.js

const decompress = require("decompress"); exports.unzipFile = function(file,destPath,callBack){ decompress(file,destPath,{ map: File => {// Here you can change the decompression location of the file, // For example, the file path in the compressed package is ${destPath}/lb-react-apps-template/ SRC /index.js const outPath = file.path.substr(file.path.indexOf('/') + 1) file.path = outPath return file }} ).then(files => { callBack() }).catch(error=>{ callBack(error) }) }Copy the code

Here you can perform the LBS init my-app test

Modify template files in your project (package.json, index.html, etc.)

Override the replaceFileContent method to change the contents of some files in the template, such as package.json’s name and index.html’s title value

function replaceFileContent(projectName,templateName){ const currentPath = process.cwd(); Json const pkgPath = path.join(currentPath, '${projectName}/package.json'); Const PKG = require(pkgPath); // change the name attribute of package.json to projectName pkg.name = projectName; fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2)); const indexPath = path.join(currentPath, `${projectName}/index.html`); let html = fs.readFileSync(indexPath).toString(); / templates/modify the title for the project name HTML = HTML. Replace (/ < title > (. *) < \ / title > / g, ` < title > ${projectName} < / title > `) fs.writeFileSync(indexPath,html); } function install(projectName){console.log(projectName)} function install(projectName){console.log(projectName)}Copy the code
Install dependencies

Rewrite the install method to create a node child process using the child_process package to perform the NPM install task. CMD on Windows, NPM. CMD on Linux, and NPM on MAC. Child_process is a nodejs package. The spawn method is used to execute system commands, the execFileSync method is used to execute files, and so on

const currentPath = process.cwd(); const npm = process.platform === 'win32' ? CMD ':' NPM '// Create a child process to execute NPM install const nodeJob = child_process.spawn(NPM, ['install'], {stdio: 'inherit', // specify the parent process communication mode CWD: path.join(currentPath,projectName)}); Nodejob.on ("close",()=>{logger.info(' created successfully! ${projectName} project is located at ${path.join(currentPath,projectName)} ') logger.info(") logger.info(' You can run the development environment by executing the following command ') logger.infoGreen(` cd ${projectName} `); logger.infoGreen(` npm run dev `); })Copy the code

Test it by performing LBS init

So here’s a simple version of scaffolding!

What doubt can concern public number private letter oh ~

If you often order take-out small friends, xiaobian to recommend a benefit. You can get a large takeaway red envelope for free every day, get it every day, no longer need to open a membership, save a breakfast every day.Click for detailsThe attached small program code is shown below: