Why build your own scaffolding

In the actual development process, we often use scaffolding developed by others to save the construction time of the project. However, when NPM didn’t have the scaffolding it wanted and we had to do it ourselves, it was important to learn how to develop front-end CLI scaffolding. Building a generic scaffolding can give you a point in your project experience.

When is scaffolding needed

In fact, most of the time from 0 to build the project can be made into templates, and the main core function of scaffolding is to use templates to quickly build a complete project structure, we only need to develop on this.

Entry requirements

Let’s deepen our understanding of the front-end scaffolding by creating the scaffolding for the JS plug-in project. Before that, let’s familiarize ourselves with the dependent libraries we need to use (click the corresponding library name to jump to the corresponding document) :

  • Chalk (Console character style)
  • Commander (implementing the NodeJS command line)
  • Download (Implement remote file download)
  • Fs-extra (Enhanced Base File Manipulation Library)
  • Handlebars (implement template character substitution)
  • Inquirer (implements command line interaction)
  • Log-symbols (provides coloring symbols for various logging levels)
  • Ora (Elegant terminal Spinner waiting for animation)
  • Update-notifier (NPM online check for updates)

Function planning

Let’s start with mind maps to plan the main commands we need for our scaffolding: init (initialize template), template (download template), mirror (switch image), upgrade (check update), the relevant map is as follows:

start

Create a new folder named js-plugin-cli and open it. Run NPM init -y to quickly initialize a package.json file and then create the corresponding file structure as follows:

Js - plugin - cli ├ ─. Gitignore ├ ─. Npmignore ├ ─. Prettierrc ├ ─ LICENSE ├ ─ README. Md ├ ─ bin │ └ ─ index. The js ├ ─ lib │ ├ ─ Init. Js │ ├ ─ config. Js │ ├ ─ download. Js │ ├ ─ mirror. Js │ └ ─ the update. The js └ ─ package. The jsonCopy the code

.gitignore,.npmignore,.prettierrc, LICENSE, readme. md are optional additional attachment files, but it is recommended to create them and set them according to your own custom.

Open the terminal in the project, first need to install dependencies, subsequent can be directly called.

yarn add -D chalk commander download fs-extra handlebars inquirer log-symbols ora update-notifier
Copy the code

Registration instructions

Node. /bin/index.js is usually executed when we want to run debugging scaffolding, but I’m still used to registering the corresponding directive, like vue init Webpack Demo, which is the scaffolding directive, and other command lines start with it. To open the package.json file, register the following instructions:

 "main": "./bin/index.js"."bin": {
    "js-plugin-cli": "./bin/index.js"
  },
Copy the code

The main file points to the entry file bin/index.js, and the js-plugin-cli under bin is our registered directive. You can set your own name (as concise as possible).

All things – v

We first write the basic code, so that the js-plugin-cli -v command can be printed in the terminal. Open the bin/index.js file and write the following code:

#! /usr/bin/env node

// Request the COMMANDER library
const program = require('commander')

// Request the value of the version field from the package.json file, with -v and --version as arguments
program.version(require('.. /package.json').version, '-v, --version')

// Parse command line arguments
program.parse(process.argv)
Copy the code

Where #! /usr/bin/env node (the first line is fixed) is mandatory. The value is used to enable the system to find and execute this line along the corresponding path.

In the debugging phase, in order to ensure the availability of jS-plugin-CLI instruction, we need to execute NPM link under the project (disconnect with NPM unlink when no instruction is needed), and then open the terminal, enter the following command and press Enter:

js-plugin-cli -v
Copy the code

At this point, version 1.0.0 should be returned, as shown:

Next we will start writing the logical code, which for maintenance purposes will be written in modules in the lib folder and referenced in bin/index.js.

Upgrade check update

Open the lib/update.js file and write the following code:

// Reference the update-notifier library to check for updates
const updateNotifier = require('update-notifier')
// Reference the chalk library for the console character style
const chalk = require('chalk')
// Import package.json file for the update-notifier library to read related information
const pkg = require('.. /package.json')

// updateNotifier is the update-notifier method. Other methods can be viewed in NPMJS
const notifier = updateNotifier({
	// Get name and version from package.json
	pkg,
    // Set check update cycle, default is 1000 * 60 * 60 * 24 (1 day)
    // Set this to 1000 milliseconds (1 second)
	updateCheckInterval: 1000,})function updateChk() {
	Notifier. Update returns Object when a version is detected
    // You can use notifier.update.latest to obtain the latest version
	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.')}}Export the updateChk() method above
module.exports = updateChk
Copy the code

Two points need to be made here: The default updateCheckInterval is 1 day, which means that the updateCheckInterval is updated once today, and the next updateCheckInterval can be updated after tomorrow. Otherwise, the updateCheckInterval will be changed to No new version is available. For example: I checked for updates at 10pm today and was told that a new version was available. Then I checked again at 4pm and was told that a new version was available. I had to wait until after 10pm tomorrow to check for updates and be told again that a new version was available. Therefore, setting updateCheckInterval to 1000 milliseconds will keep each check update up to date. In addition, update-notifier checks the update mechanism by using the package.json file name and version fields: It retrieves the latest version number of the library from NPMJS using the name field value, and then compares it with the version value of the local library. If the version number of the local library is lower than the latest version number of the NPMJS, an update prompt will be given. To declare the upgrade command, open the bin/index.js file and add the following code in the appropriate place:

/ lib/request/update. Js
const updateChk = require('.. /lib/update')

// upgrade checks for updates
program
	// Declare the command
	.command('upgrade')
    // The description is displayed when the help message is displayed
	.description("Check the js-plugin-cli version.")
	.action(() = > {
    	// Perform the operations in lib/update.js
		updateChk()
	})
Copy the code

The added code should look like this:

Remember to put Program.parse (process.argv) at the end.

After adding the code, open the console and enter the command js-plugin-cli upgrade to view the result:

In order to test the effect, I changed the name of package.json in js-plugin-cli to vuepress-creator. The version default is 1.0.0. The latest version of vuepress-Creator scaffolding on NPMJS is 2.x, so there will be an update prompt.

Mirror Switch mirror link

We usually put the template on Github, but downloading the template from Github is not usually slow in China, so I consider putting the template on Vercel. However, in order to avoid the problem that users in some regions cannot download templates normally due to network problems, we need to make the template link definable. Users can then customize the template link, change it to their own stable image hosting platform, or even download the template and maintain it on their own server. Json file is created locally to store the information, not manually, but by scaffolding. The logic is as follows:

So we also need to create the config.js file in the lib folder to generate the default configuration file. Open the lib/config.js file and add the following code:

// Request fs-extra library
const fse = require('fs-extra')

const path = require('path')

Declare the contents of the configuration file
const jsonConfig = {
  "name": "js-plugin-cli"."mirror": "https://zpfz.vercel.app/download/files/frontend/tpl/js-plugin-cli/"
}

// Splice config.json full path
const configPath = path.resolve(__dirname,'.. /config.json')

async function defConfig() {
  try {
  	// Save jsonConfig contents as JSON files using fs-extra encapsulation method
    await fse.outputJson(configPath, jsonConfig)
  } catch (err) {
    console.error(err)
    process.exit()
  }
}

// Export the defConfig() method above
module.exports = defConfig
Copy the code

Note that we do not directly use the built-in FS library, we recommend to use the enhanced library FS-extra, fS-extra in addition to encapsulating the original basic file operation method, there is also a convenient JSON file read and write method. Open the lib/mirror.js file and add the following code:

// Request the log-symbols library
const symbols = require('log-symbols')
// Request fs-extra library
const fse = require('fs-extra')

const path = require('path')

// Request the config.js file
const defConfig = require('./config')
// Splice config.json full path
const cfgPath = path.resolve(__dirname,'.. /config.json')

async function setMirror(link) {
  // Check whether the config.json file exists
  const exists = await fse.pathExists(cfgPath)
  if (exists){
  	// Write configuration directly if it exists
    mirrorAction(link)
  }else{
    // Initialize configuration before writing configuration if it does not exist
    await defConfig()
    mirrorAction(link)
  }
}

async function mirrorAction(link){
  try {
    // Read the config.json file
    const jsonConfig = await fse.readJson(cfgPath)
    // Write the passed parameter link to the config.json file
    jsonConfig.mirror = link
    // Write the config.json file again
    await fse.writeJson(cfgPath, jsonConfig)
    // Wait until the configuration succeeds
    console.log(symbols.success, 'Set the mirror successful.')}catch (err) {
    // If an error occurs, an error message is displayed
    console.log(symbols.error, chalk.red(`Set the mirror failed. ${err}`))
    process.exit()
  }
}

// Export the setMirror(link) method above
module.exports = setMirror
Copy the code

Attention should be paid to async and await. Here async/await is used. For other related writing methods, please refer to Fs-extra. Async precedes functions by default, and await is added as needed, for example:

.const jsonConfig = await fse.readJson(cfgPath)
  jsonConfig.mirror = link
  await fse.writeJson(cfgPath, jsonConfig)
  console.log(symbols.success, 'Set the mirror successful.')...Copy the code

We need to wait for fs-extra to finish reading before we can proceed to the next step. If we do not wait, jsonConfig.mirror = link statement will continue to execute, which will result in the change of the json structure passed in. “Await fse.writejson (cfgPath, jsonConfig)”. If we remove “await”, it means that we are still writing json data (assuming it takes 1 minute to write data) before we proceed to the next statement. “Set the mirror successful.”

As usual, we also need to declare the mirror command, open the bin/index.js file, and add the following code in the appropriate place:

/ request/lib/mirror. Js
const setMirror = require('.. /lib/mirror')

// mirror Switches the mirror link
program
	.command('mirror <template_mirror>')
	.description("Set the template mirror.")
	.action((tplMirror) = > {
		setMirror(tplMirror)
	})
Copy the code

Open the console and type js-plugin-cli mirror to view your mirror link:

At this point, the config.json file should have been generated under the project, and the relevant content should be:

{
  "name": "js-plugin-cli"."mirror": "https://zpfz.vercel.app/download/files/frontend/tpl/js-plugin-cli/"
}
Copy the code

Download Download/Update the template

Many tutorials on the web refer to the Download-Git-repo library when talking about scaffolding download templates, but I chose the Download library because it provides a more free way to download. After all, the Download-Git-repo library is mainly for Github and other platforms, while the Download-Git-repo library can download any linked resource and even has powerful decompression capabilities (no need to install other decompression libraries). Lib /download.js will download/update the template and overwrite it to keep it up to date, regardless of whether the template exists locally.

Open the lib/download.js file and add the following code:

// Request the Download library to download the template
const download = require('download')
// Request the ORA library to implement the wait animation
const ora = require('ora')
// Request the Chalk library to implement the console character style
const chalk = require('chalk')
// Request the fs-extra library for file manipulation
const fse = require('fs-extra')
const path = require('path')

// Request the config.js file
const defConfig = require('./config')

// Splice config.json full path
const cfgPath = path.resolve(__dirname,'.. /config.json')
// Splice the full path of the template folder
const tplPath = path.resolve(__dirname,'.. /template')

async function dlTemplate() {
  // Refer to the mirror.js main code comment above
  const exists = await fse.pathExists(cfgPath)
  if (exists){
  	// remember to await. Use async/await when init.js is called
    await dlAction()
  }else{
    await defConfig()
    / / same as above
    await dlAction()
  }
}

async function dlAction(){
  // Clear the contents of the template folder. See fs-extra's readme.md for usage
  try {
    await fse.remove(tplPath)
  } catch (err) {
    console.error(err)
    process.exit()
  }

  // Read the configuration to get the mirror link
  const jsonConfig = await fse.readJson(cfgPath)
  // The Spinner is initialized
  const dlSpinner = ora(chalk.cyan('Downloading template... '))
  
  // Start the wait animation
  dlSpinner.start()
  try {
    // Download the template and unzip it
    await download(jsonConfig.mirror + 'template.zip', path.resolve(__dirname,'.. /template/'), {extract:true});
  } catch (err) {
    // Download failed
    dlSpinner.text = chalk.red(`Download template failed. ${err}`)
    // Terminates the wait animation and displays the X flag
    dlSpinner.fail()
    process.exit()
  }
  // Download success prompt
  dlSpinner.text = 'Download template successful.'
  // End the waiting animation and display the bookmark
  dlSpinner.succeed()
}

// Export the dlTemplate() method above
module.exports = dlTemplate
Copy the code

Remove () is used to empty the template folder (regardless of whether the template folder exists or not, because the folder does not exist), and then executes the wait animation and requests the download. The template file name is fixed to template.zip. Extract :true in the download statement Process.exit () is added in two places, which means that the process will be forced to exit as soon as possible (sort of like a return, except that process.exit() terminates the entire process), even if there are incomplete asynchronous operations. Take the second process.exit(), for example. When you mirror the link in a 404 or other state, it will return you with an error message and exit the process without continuing with the dlSpinner. We also need to declare the template command, open the bin/index.js file, and add the following code where appropriate:

/ request/lib/download. Js
const dlTemplate = require('.. /lib/download')

// template Downloads/updates the template
program
	.command('template')
	.description("Download template from mirror.")
	.action(() = > {
		dlTemplate()
	})
Copy the code

Open the console and run the js-plugin-cli template command to view the effect:

The image above returns a 404 Not Found error because I haven’t uploaded the template file to the server. It will display correctly once the template is uploaded.

Init Initializes the project

Next comes our main init command, init initializes the project with more logic than the other templates, so we parse it last. The command to initialize the project is js-plugin-cli init the project name, so we need to use the project name as the folder name and also the name of package.json in the project (lowercase only, so need to convert). Since templates are used to develop JS plug-ins, we need to throw global function names (such as Antd for import Antd from ‘ant-design-vue’), so we also need to throw the global function names of templates to the user to define through console interaction. After completing the interaction, the scaffold will replace the user’s input into the template content. The complete logical diagram is as follows:

Open the lib/init.js file and add the following code:

// Request the fs-extra library for file manipulation
const fse = require('fs-extra')
// Request the ORA library to wait for animation while initializing the project
const ora = require('ora')
// Request the chalk library
const chalk = require('chalk')
// Request the log-symbols library
const symbols = require('log-symbols')
// Request the Inquirer library for console interaction
const inquirer = require('inquirer')
// Request the handlebars library to replace the template character
const handlebars = require('handlebars')

const path = require('path')

// Request the download.js file if the template is not locally available
const dlTemplate = require('./download')

async function initProject(projectName) {
	try {
		const exists = await fse.pathExists(projectName)
		if (exists) {
        	// Alert users when items have the same name
			console.log(symbols.error, chalk.red('The project already exists.'))}else {
        	// Perform the console interaction
			inquirer
				.prompt([
					{
						type: 'input'.// Type, other types see official documentation
						name: 'name'.// Name, used to index the value of the current name
						message: 'Set a global name for javascript plugin? '.default: 'Default'.// Default value, used when the user does not enter
					},
				])
				.then(async (answers) => {
                	// The Spinner is initialized
					const initSpinner = ora(chalk.cyan('Initializing project... '))
                    // Start the wait animation
					initSpinner.start()
                    
					// Concatenate the template folder path
					const templatePath = path.resolve(__dirname, '.. /template/')
                    // Returns the current working directory of the Node.js process
					const processPath = process.cwd()
                    // Change the project name to lowercase
					const LCProjectName = projectName.toLowerCase()
                    // Splice the full path of the project
					const targetPath = `${processPath}/${LCProjectName}`
                    
					// Check whether the template path exists
					const exists = await fse.pathExists(templatePath)
					if(! exists) {// If the template does not exist, wait for the template to download and then execute the following statement
						await dlTemplate()
					}
                    
                    // Wait for the template file to be copied to the corresponding path
         			try {
						await fse.copy(templatePath, targetPath)
					} catch (err) {
						console.log(symbols.error, chalk.red(`Copy template failed. ${err}`))
						process.exit()
					}
                    
          			// Prepare the template characters to be replaced
                    const multiMeta = {
                      project_name: LCProjectName,
                      global_name: answers.name
                    }
                    // Prepare the file to be replaced
                    const multiFiles = [
                      `${targetPath}/package.json`.`${targetPath}/gulpfile.js`.`${targetPath}/test/index.html`.`${targetPath}/src/index.js`
                    ]
                    
					// Use conditional loops to replace template characters with files
                    for (var i = 0; i < multiFiles.length; i++){// Try {} catch {} to terminate the Spinner in case of an error
                        try {
                        	// Wait to read the file
                            const multiFilesContent = await fse.readFile(multiFiles[i], 'utf8')
                            // Wait to replace file, handlebars.compile(original file contents)(template character)
                            const multiFilesResult = await handlebars.compile(multiFilesContent)(multiMeta)
                            // Wait for the output file
                            await fse.outputFile(multiFiles[i], multiFilesResult)
                        } catch (err) {
                        	// If something goes wrong, Spinner changes the text message
                            initSpinner.text = chalk.red(`Initialize project failed. ${err}`)
                            // Terminates the wait animation and displays the X flag
                            initSpinner.fail()
                            // Exit the process
                            process.exit()
                        }
                    }
                    
					// If successful, Spinner changes the text message
					initSpinner.text = 'Initialize project successful.'
                    // End the waiting animation and display the bookmark
					initSpinner.succeed()
                    console.log(`
To get started:

	cd ${chalk.yellow(LCProjectName)}
	${chalk.yellow('npm install')} or ${chalk.yellow('yarn install')}
	${chalk.yellow('npm run dev')} or ${chalk.yellow('yarn run dev')}
					`)
				})
				.catch((error) = > {
					if (error.isTtyError) {
						console.log(symbols.error,chalk.red("Prompt couldn't be rendered in the current environment."))}else {
						console.log(symbols.error, chalk.red(error))
					}
				})
		}
	} catch (err) {
		console.error(err)
		process.exit()
	}
}

Export the initProject(projectName) method above
module.exports = initProject
Copy the code

The lib/init.js code is relatively long, so it is recommended to familiarize yourself with the above logic diagram. Extract the main fragment analysis: Inquirer Field name in inquirer. Prompt is similar to key. When you need to obtain this value, you should obtain the corresponding value of answers.key (answers name depends on.then(Answers => {})).

inquirer.prompt([
  {
    type: 'input'.// Type, other types see official documentation
    name: 'theme'.// Name, used to index the value of the current name
    message: 'Pick a theme? '.default: 'Default'.// Default value, used when the user does not enter
  },
]).then(answers= > {})
Copy the code

The corresponding value to get above should be answers.theme.

Handlebars {{name}} {handlebars.compile}} {{name}} {handlebars.compile}} {key:value}, then key corresponds to the definition name, value corresponds to the template character to be replaced, for example:

const multiMeta = {
  project_name: LCProjectName,
  global_name: answers.name
}
Copy the code

The above code means that the string to be modified in the template file is of the form {{project_name}} or {{global_name}} and, when replaced, will be the corresponding template character. Here is the template file:

Next we declare the init command, open the bin/index.js file, and add the following code in the appropriate place:

/ request/lib/init. Js
const initProject = require('.. /lib/init')

// init initializes the project
program
	.name('js-plugin-cli')
	.usage('<commands> [options]')
	.command('init <project_name>')
	.description('Create a javascript plugin project.')
	.action(project= > {
		initProject(project)
	})
Copy the code

Open the console, type js-plugin-cli init your project name to see the effect:

The scaffolding is now complete and can then be published to NPM for global installation (remember NPM unlink).

Write it at the very end

This article took a few days (including writing scaffolding demo time) to edit, time is more rushed, if the statement is not clear or error, welcome to dig friends pointed out oh ~ finally attached project source: JS-plugin-CLI, scaffolding has been released to NPM, welcome small partners try oh! Time gate: js-plugin-CLI

🏆 nuggets technical essay | double festival special articles