preface

In my daily work, I often use CTRL + C and CTRL + V with the same project frame or configuration, such as:

  • To build a basic framework for a new project, start with a project template you have writtencloneGo down, CV to the new Git repository, andpush
  • When writing a demo or small project, CV a similar demo and delete some code

This repetition takes up a lot of my time, especially if I’m in a hurry to do a presentation or write a demo. Scaffolding is used to quickly build the infrastructure of the project, eliminating the need for duplicate CV processes. We can implement a CLI tool to generate corresponding project templates through simple user queries, eliminating the process of repeated creation during development.

Preparing to develop the CLI

  • Node: My development environment node version is 12.x, so the dependencies used in scaffolding are the lower versions introduced in accordance with the CommonJS specification. If you prefer to use the ESM specification, you can make Node support ESM syntax in later (>13.2.0) versions by specifying “type”:”module” in the package.json file.

  • Dependency packages used:

    The package name role
    path Node’s built-in module for handling file paths
    fs A module built into Node to process files
    commander Command line custom command
    chalk Console output styling
    ora The console displays the loading animation
    inquirer The user information
    prettier Formatting file styles
    rimraf Delete files and folders
    download-git-repo Git Remote Download
    ejs A template engine
    execa Executing system commands
  • NPM account: Used to publish to the community for use by others or yourself on other office devices

Set up a simple CLI project

  1. Execute in a blank foldernpm initGenerate package. Json. I named the project yuwuwu-cli and created the file manually with the following directory structure:
├─ ├─ download.txt TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXT TXTCopy the code
  1. In the package. Add jsonbin:{"yuwuwu": "./bin/index.js"}, specifying the file entry to which the command is to be executedyuwuwuIt can be used as a command.
//package.json
{
  "name": "yuwuwu-cli"."version": "1.0.0"."description": scaffolding."main": "index.js"."directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "yuwuwu": "./bin/index.js"
  },
  "author": "yuwuwu"."license": "ISC"."dependencies": {}}Copy the code
  1. index.jsFile first line added#! /usr/bin/env nodeIn which case node will be called to execute the file.
#! /usr/bin/env node
console.log("Hello yuwuwu-cli")
Copy the code
  1. Local debug, executenpm link, then you can use the library globallyyuwuwuThe console will outputHello yuwuwu-cli

  1. Publish to the community, execute firstnpm loginLog in to your NPM account, you can go to the official website to register. After logging innpm publishRelease. Notice when publishingpackage.jsonThe name in the file cannot be the same as the name of the existing NPM package. We can improve the readme.md file to make it easier for other users to use your tool.
/ / to login first
npm login
/ / release again
npm publish
Copy the code
  1. After the release is successful, it can be stored on any devicenpm install yuwuwu-cli -gIt is available after global installationyuwuwuThis command.

Build your own CLI

First of all, according to the function of scaffolding, comb out the whole code process and sort out todolist:

  1. Creating project files
  2. The user asks to select a template
  3. Download the project template
  4. Installation project dependencies

1. Create a project file

1.1 Receiving Command Line Parameters

Refer to vue-CLI, vue scaffolding has create command to create a project, and commonly used -v, config and other commands, we need to use commander to achieve this command.

const version = require(".. /package.json").version;
const program = require("commander");


program
  .command("init <project-name>")
  .description("Initialize the project file")
  .action((projectName) = > {
    console.log("Project Name:" + projectName);
    cliStart()
  });

program
  .on("--help".function () {
    console.log();
    console.log(Yuwuwu init 
      );
    console.log();
  });

program
  .version(version, '-v,--version')
  .parse(process.argv);
if (program.args.length < 2) program.help();

async function cliStart() {
    //todo
    // 1. Create project files
    // 2. The user asks to select a template
    // 3. Download the project template
    // 4. Install project dependencies
}
Copy the code

Init , –help, -v/ -v/–version. Enter the command on the console and see that we have successfully obtained the information we want.

This doesn’t look pretty enough to show what information is important, so we can use the chalk dependency package, which can change the color of the console output. Color the information we think is important.

  chalk.green("Project Name:" + projectName)
Copy the code

1.2 Creating a Folder

When creating a folder, you may encounter an existing folder problem. At this time, you need to ask the user whether to overwrite the folder with the same name. User queries need to be implemented using inquirer, while operating folders can be implemented using fs and path modules.

async function cliStart() {
    //todo
    // 1. Create project files
    await mkdirByProjectName()
    // 2. The user asks to select a template
    // 3. Download the project template
    // 4. Install project dependencies
}
async function mkdirByProjectName() {
    if (fs.existsSync(program.args[0]) {console.log(chalk.red(program.args[0] + "Folder already exists"));
        let answers = await isRemoveDirQuestion()
        if(answers.ok) { rimraf.sync(getProjectName()); fs.mkdirSync(getProjectName()); }}else{ fs.mkdirSync(getProjectName()); }}function isRemoveDirQuestion() {
    return inquirer.prompt([
        {
            type: "confirm".message: Do you want to overwrite the original folder?.name: "ok",})}function getProjectName() {
    return path.resolve(process.cwd(), program.args[0])}Copy the code

Fs.existssync () is used to check if the folder exists. If it does not, fs.mkdirsync () is created directly. If it does, inquirer.prompt() will issue a query to the user. If the user agrees to overwrite rimraf.sync(), delete the original folder and create again.

2. The user asks to select a template

This step is relatively easy, we just need to ask the user, I listed here in my daily development is often CV to several projects.

  • Vue2 + Vant Mobile terminal template
  • Vue2 + Element Background management template
  • Customize the Node template

The main difference between these templates is in the next step. Some templates pull the remote repository code directly from Github, while others customize the project based on requirements.

async function cliStart() {
    //todo
    // 1. Create project files
    await mkdirByProjectName()
    // 2. The user asks to select a template
    const {choice} = await choiceTemplateQuestion()
    // 3. Download the project template
    // 4. Install project dependencies
}
/ /...

function choiceTemplateQuestion() {
    return inquirer.prompt([
        {
            type: "list".name: "choice".message: "Select template to use :".choices: [{value: "github:yuwuwu/vue-mobile-template".name: "Vue2 + Vant mobile Template"}, {value: "github:yuwuwu/vue-pc-template".name: "Vue2 + Element Background Management template"}, {value: "node".name: "Custom Node template",},],},])}Copy the code

The execution effect is as follows:

3. Download the project template

3.1 Downloading a Remote Template

Download the remote template using the download-git-repo dependency. Note that it doesn’t support Promises, so we’ll simply wrap it to support Promises and make the cliStart method look good. It takes time to download the template from Git, but the console does not have any interaction at this time. Here we use the ora dependency package, which can output loading in the console to give the user a sense of waiting.

async function cliStart() {
    //todo
    // 1. Create project files
    await mkdirByProjectName()
    // 2. The user asks to select a template
    const { choice } = await choiceTemplateQuestion()
    // 3. Download the project template
    switch (choice) {
        case 'node':
            await createNodeTemplate()
            break;
        default:
            await downloadByGit(choice);
    }
    // 4. Install project dependencies
}
/ /...
function downloadByGit(url) {
    const loading = ora("downloading").start()
    return new Promise((res, rej) = > {
        download(url, getProjectName(), { clone: false }, function (err) {
            loading.stop()
            if (err) {
                console.log(chalk.red(err));
                rej()
                process.exit(1)}console.log(chalk.green('download success! ')); res() }); })}Copy the code

Now that you have a scaffold for pulling different project templates, you can run it to see what it looks like:Open the test folder and find that a bunch of project files have already been downloaded.

3.2 Customizing node Templates

Sometimes project templates also need to be customized. For example, IN my daily work, I often create some Node projects for demonstration. Project A needs CORS and Body-Parser modules, and project B needs FS modules. Nodeprograms generally contain the main entry file index.js and the configuration file package.json. We only need to dynamically generate the contents of these two files according to the user’s query content to achieve a custom template. The generated files can be rendered using fs modules, and the contents of the files can be rendered using EJS code templates. Prettier could be used to format the content.

// index.ejs
// Omit part of the repeated logic of the code, the full code can be viewed at the end
const express = require('express');
const app = express()

<% if (modules.indexOf("cors") > -1) {- % >const cors = require('cors'); The < % %} - >/ /...
const server = app.listen(<%= port %> , '0.0.0.0'.() = > {
  const host = server.address().address;
  const port = server.address().port;
  console.log('app start listening at http://%s:%s', host, port);
});
Copy the code
{"name": "<%= name %>", "version": "1.0.0", "description": "<%= description %>", "main": "index.js", "directories": { "test": "test" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js" }, "author": "<%= author %>", "license": "ISC", "dependencies" : {< % if (modules. IndexOf (cors) > 1) {- % > "cors" : "^ 2.8.5", < % %} - > "express" : "^ 4.17.2"}}Copy the code
async function cliStart() {
    //todo
    // 1. Create project files
    await mkdirByProjectName()
    // 2. The user asks to select a template
    const { choice } = await choiceTemplateQuestion()
    // 3. Download the project template
    switch (choice) {
        case 'node':
            await createNodeTemplate()
            break;
        default:
            await downloadByGit(choice);
    }
    // 4. Install project dependencies
}
/ /...
async function createNodeTemplate() {
  let answers = await nodeProjectQuestion()
  fs.writeFileSync(getProjectName() + "/index.js", getIndexTemplate(answers))
  fs.writeFileSync(getProjectName() + "/package.json", getPackageTemplate(answers))
}
function getIndexTemplate(config){
  const ejsTemplateData = fs.readFileSync(path.resolve(__dirname,"./nodeTemplate/index.ejs"))
  const indexTemplateData = ejs.render(ejsTemplateData.toString(),{
    port: config.port,
    modules: config.modules
  })
  return prettier.format(indexTemplateData,{ parser: "babel"})}function getPackageTemplate(config){
  const ejsTemplateData = fs.readFileSync(path.resolve(__dirname,"./nodeTemplate/package.ejs"))
  const packageTemplateData = ejs.render(ejsTemplateData.toString(),{
    name: config.name,
    description: config.description,
    author: config.author,
    modules: config.modules
  })
  return prettier.format(packageTemplateData,{ parser: "json"})}Copy the code

4. Install project dependencies

At this point scaffolding generation of project templates is complete, leaving some optimizations such as NPM install and NPM start. Execa can be used to execute system commands in code.

async function cliStart() {
    //todo
    // 1. Create project files
    await mkdirByProjectName()
    // 2. The user asks to select a template
    const { choice } = await choiceTemplateQuestion()
    // 3. Download the project template
    switch (choice) {
        case 'node':
            await createNodeTemplate()
            break;
        default:
            await downloadByGit(choice);
    }
    // 4. Install project dependencies
    await installModules()
}
/ /...
async function installModules() {
  const loading = ora("install...").start()
  await execa("yarn", { cwd: getProjectName() }, ["install"])
    .then(() = > {
      loading.succeed("install")
      console.log(chalk.green("install success!!!"))
    })
    .catch((err) = > {
      console.log(chalk.red(err))
      loading.stop()
    })
}
Copy the code

Execa (” YARN “, {CWD: getProjectName()}, [“install”]) execa(” YARN “, {CWD: getProjectName()}, [“install”])

Once this is done, our scaffolding is fully functional and can be updated and re-published to NPM so that it can be used on any device.

At the end

The function of scaffolding is not only these, I just completed some basic functions of scaffolding. Here is a scaffolding idea, I hope to help you work. In daily development, there are still many scenarios for repeated operations, such as repeated CV. We should also adopt such automatic and engineering thinking in daily coding to reduce repetitive work and improve efficiency and leave work early.

See github: Yuwuwu – CLI for complete code