Scaffolding construction as the company’s infrastructure work, has a very important role. Everyday scaffolding like vue-CLI, Angular-CLI, etc., is built quickly with simple initialization commands. It is necessary for us to systematically understand and master the knowledge of scaffolding construction.

  • The article relates to the scaffolding source link: github.com/llz1990/llz… (Please help star)
  • Finally submit NPM source: www.npmjs.com/package/llz…

Simple application based on vue-CLI to understand scaffolding

Scaffolding is to ask some simple questions at startup, and the user selects the result to render the corresponding template file, the basic workflow is as follows:

  1. Ask the user questions through command line interaction
  2. Generate files based on the results of user answers

When using vue-CLI, we first run the created command to ask the user a question, and the user can choose for himself.We see that the project template file we need is finally generated.Following the vue-CLI process, we can also build a scaffold ourselves.

Build your own scaffolding

1. Project creation

Start by simply creating a project structure with NPM init. I named my own project llzscc_cli(since pushing to NPM cannot duplicate the name). Add a startup file bin/cli.js, which writes commander commands.

├─ ├─ download.txt └─ download.txtCopy the code

Then configure the scaffold’s package.json file

{
  "name": "llzscc_cli"."version": "1.0.0"."description": scaffolding."main": "index.js"."bin": {
    "run": "./bin/cli.js" // Set the path of the startup file. Run is the alias
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": {
    "name": "llz"
  },
  "license": "MIT"
}
Copy the code

A quick edit of our cli.js

#! /usr/bin/env node
console.log('~ working ~');
Copy the code

Run the NPM link command on the terminal to link to the global for easy debugging. To print the output, run:

~/Desktop/Cli /llzscc_cli ->run ~ working ~ # Print the contentCopy the code

2. Create a scaffold startup command

To do this, we need to use the COMMANDER dependency to output the terminal command line. Refer to vue-CLI commands such as create and config, we need to provide similar instructions to complete the logical implementation.

First install the COMMANDER dependency package:

$ npm install commander --save
Copy the code

After installation, you can edit the cli.js content:

#! /usr/bin/env node

const program = require('commander')

program
  // Define commands and parameters
  .command('create [name]')
  .description('create a new project')
  -for --force Forcibly creates a directory. If the created directory exists, the directory is overwritten directly
  .option('-f, --force'.'overwrite target directory if it exist')
  .action((name, options) = > {
    // Print the result to output the project name entered manually by the user
    console.log('name:',name)
  })
  
program
   // Set the version number
  .version(`vThe ${require('.. /package.json').version}`)
  .usage('<command> [option]')
  
// Parses the parameters passed by the user to execute the command
program.parse(process.argv);
Copy the code

As shown below, input relevant commands on the terminal to verify:

->run create
error: missing required argument 'name'->run create my-project >>> name: my-project options: {} ->run create my-project -f my-project options: {force: true} ->run create my-project --force run result >>> name: my-project options: {force: true }
Copy the code

Create a lib folder containing the main logical implementation and create create.js in that folder

// lib/create.js

module.exports = async function (name, options) {
  // Verify that the value is correctly fetched
  console.log('create success', name);
}
Copy the code

Use create.js in cli.js

// bin/cli.js
program
  .command('create [name]')
  .description('create a new project')
  .option('-f, --force'.'overwrite target directory if it exist') 
  .action((name, options) = > {
    require('.. /lib/create.js')(name, options)    // Import the create.js file
  })
Copy the code
->run create my-project
>>> create success my-project 
Copy the code

When creating a project file directory, there is another question to consider: if the directory already exists, what to do with the existing directory? There are several options: create a new project file directory if none exists; If there is, whether to delete directly or replace with a new project file directory (in this logical judgment process involves an interrogation process of the scaffolding core function, we need to provide the user with command selection, this process will be implemented later). The current process involves nodeJS handling files, and we introduce dependency package fs-extra to improve create.js:

$ npm install fs-extra --save
Copy the code
// lib/create.js
const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
    const cwd = process.cwd(); // Select a directory
    const targetAir = path.join(cwd, name); // Directory address to be created
  // Check whether the directory already exists.
  if (fs.existsSync(targetAir)) {
    // Is it mandatory?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // TODO: asks the user if they are sure to overwrite}}}Copy the code

We have created the create command to create a new project directory. Can we continue to develop new commands? Do the same for cli.js. For example, if we configure a config command, we can add code:

// bin/cli.js.// Configure the config command
program
    .command('config [value]')
    .description('inspect and modify the config')
    .option('-g, --get <path>'.'get value from option')
    .option('-s, --set <path> <value>')
    .option('-d, --delete <path>'.'delete option from config')
    .action((value, options) = > {
        console.log('Custom config command:', value); })...Copy the code

LLZSCC -cli run –help

> run -- help -- -- -- -- -- -- -- -- -- -- the following follow commander orders -- -- -- -- -- -- -- -- -- the Usage: cli < command > [option]Options:
  -V, --version             output the version number
  -h, --help                output usage information 

Commands:
  create [options] [name]   create a new project     
  config [options] [value]  inspect and modify the config
Copy the code

For the output –help message, we can make some nice style, introduce rely on chalk, figlet, we print an interesting pattern:

// bin/cli.js
// Print an interesting help
program
    .on('--help'.() = > {
        // Use figlet to draw the Logo
        console.log('\r\n' + figlet.textSync('zhurong', {
            font: 'Ghost'.horizontalLayout: 'default'.verticalLayout: 'default'.width: 80.whitespaceBreak: true
        }));
        // Add description information
        console.log(`\r\nRun ${chalk.cyan(`roc <command> --help`)} show details\r\n`)})Copy the code

3. Ask users about function implementation

In the previous step, we left a question: how to implement the operation of asking the user when judging the file directory? Here we introduce a dependency on Inquirer that implements the following logic:

// Whether the directory already exists:
    if (fs.existsSync(targetAir)) {
        if (options.force) {
            await fs.remove(targetAir);
        } else {
            // The terminal output asks the user whether to overwrite:
            const inquirerParams = [{
                name: 'action'.type: 'list'.message: 'The target file directory already exists, please select the following:'.choices: [{name: 'Replace current directory'.value: 'replace'},
                    { name: 'Remove existing directory'.value: 'remove' }, 
                    { name: 'Cancel current action'.value: 'cancel'}}]];let inquirerData = await inquirer.prompt(inquirerParams);
            if(! inquirerData.action) {return;
            } else if (inquirerData.action === 'remove') {
                // Remove an existing directory
                console.log(`\r\nRemoving... `)
                await fs.remove(targetAir)
            }
        }
    }
Copy the code

To test the effect, add a folder SRC and run create SRC. We can see that an inquiry choice appears, which is the desired effect.

4. Use the API provided by Git to pull template information

As you can see in the figure below, vuejs has many existing templates. For example, we need to pull down the existing template of Vuejs, and then ask the user which template to choose, and also provide the version information of the template to ask the selection. How to do this process?Git provides the following API interfaces to obtain template information and version information:https://api.github.com/orgs/${projectName}/repos, interface for obtaining template version information:https://api.github.com/repos/${projectName}/${repo}/tags. We create an http.js file handling interface in the lib folder.

const axios = require('axios');

axios.interceptors.response.use(res= > {
    return res.data;
})

/** * get the template list *@returns Promise* /
async function getRepoList() {
    return axios.get('https://api.github.com/orgs/vuejs/repos')}/** * Obtain version information *@param {string} Repo template name *@returns Promise* /
async function getTagList(repo) {
    return axios.get(`https://api.github.com/repos/vuejs/${repo}/tags`)}module.exports = {
    getRepoList,
    getTagList
}
Copy the code

We create a file generator.js to handle the logic of pulling the template and reference it in create.js.

// lib/Generator.js

class Generator {
  constructor (name, targetDir) {// Directory name
    this.name = name;
    // Create the location
    this.targetDir = targetDir;
  }

  // Core creation logic
  create(){}}module.exports = Generator;
Copy the code
// lib/create.js.const Generator = require('./Generator')

module.exports = async function (name, options) {
  // Execute the create command

  // Directory selected by the current command line
  const cwd  = process.cwd();
  // Directory address to be created
  const targetAir  = path.join(cwd, name)

  // Does the directory already exist?
  if (fs.existsSync(targetAir)) {
    ...
  }

  // Create the project
  const generator = new Generator(name, targetAir);

  // Start creating the project
  generator.create()
}
Copy the code

Implement specific logic in generator.js: 1. 2. Select the version information about the template. 3. Add animation effects; And then you can see that the terminal query is what we want it to be.

const {
    getRepoList,
    getTagList
} = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')
const chalk = require('chalk')

// Add loading animation
async function wrapLoading(fn, message, ... args) {
    // Use ORA to initialize, passing in the prompt message message
    const spinner = ora(message);
    // Start loading animation
    spinner.start();

    try {
        // execute the passed method fn
        const result = awaitfn(... args);// Place choose a tag to create project
        spinner.succeed('Request succeed !!! ');
        return result;
    } catch (error) {
        // The status is changed to failed
        spinner.fail('Request failed, refetch ... ', error)
    }
}

class Generator {
    constructor(name, targetDir) {
        // Directory name
        this.name = name;
        // Create the location
        this.targetDir = targetDir;
    }

    // Get the template selected by the user
    // 1) Pull template data from remote
    // 2) The user selects the name of his newly downloaded template
    // 3) return The name selected by the user

    async getRepo() {
        // 1) Pull template data from remote
        const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
        if(! repoList)return;

        // Filter the name of the template we need
        const repos = repoList.map(item= > item.name);

        // 2) The user selects the name of his newly downloaded template
        const {
            repo
        } = await inquirer.prompt({
            name: 'repo'.type: 'list'.choices: repos,
            message: 'Please choose a template to create project'
        })

        // 3) return The name selected by the user
        return repo;
    }

    // Get the version selected by the user
    // 1) Based on the repO result, pull the corresponding tag list remotely
    // 2) The user selects the tag to download
    // 3) return The tag selected by the user

    async getTag(repo) {
        // 1) Based on the repO result, pull the corresponding tag list remotely
        const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
        if(! tags)return;

        // Filter the tag name we need
        const tagsList = tags.map(item= > item.name);

        // 2) The user selects the tag to download
        const {
            tag
        } = await inquirer.prompt({
            name: 'tag'.type: 'list'.choices: tagsList,
            message: 'Place choose a tag to create project'
        })

        // 3) return The tag selected by the user
        return tag
    }

    // Core creation logic
    // 1) Get the template name
    // 2) Get the tag name
    // 3) Download the template to the template directory
    async create() {

        // 1) Get the template name
        const repo = await this.getRepo();

        // 2) Get the tag name
        const tag = await this.getTag(repo);

        console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)}}module.exports = Generator;
Copy the code

5. Download the remote template

After the template information is pulled from the previous step, you need to download the remote template and introduce the dependency download-git-repo. Note that it does not support Promises. So we need to promise it using the promisify method in the Util module. We implement the core function module download function:

// lib/Generator.js.const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo') // Promise is not supported

// Add loading animation
async function wrapLoading(fn, message, ... args) {... }class Generator {
  constructor (name, targetDir){
    ...

    // Make a promise to download-git-repo
    this.downloadGitRepo = util.promisify(downloadGitRepo); }...// Download the remote template
  // 1) Add the download address
  // 2) Call the download method
  async download(repo, tag) {

    // 1) Add the download address
    const requestUrl = `vuejs/${repo}${tag?The '#'+tag:' '}`;

    // 2) Call the download method
    await wrapLoading(
        this.downloadGitRepo, // Remote download method
        'waiting download template'.// Load the prompt
        requestUrl, // Parameter 1: download address
        path.resolve(process.cwd(), this.targetDir)) // Parameter 2: creation location
  }

  // Core creation logic
  // 1) Get the template name
  // 2) Get the tag name
  // 3) Download the template to the template directory
  // 4) Template usage tips
  async create() {

        // 1) Get the template name
        const repo = await this.getRepo();

        // 2) Get the tag name
        const tag = await this.getTag(repo);

        // 3) Download the template to the template directory
        await this.download(repo, tag);

        console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)}}module.exports = Generator;
Copy the code

At this point, a simple scaffolding has been preliminarily completed. To test the effect, run create SRC. The vUE module is downloaded when SRC file is shown in the figure. The terminal shows that the module is downloaded and the scaffold has the following functions:

6. Release projects

The above are local tests, the actual use of the time, need to publish to the NPM repository, through the NPM global installation, directly to the target directory below to create the project, how to publish?

  1. First go to the NPM official website to create an account. After registering our account, we need to log in locally and publish our component
  2. Through the terminal command NPM login. Then enter your account, password, email when you see in the consoleLogged in as <Username> on https://registry.npmjs.orgLogin successful
  3. If you publish a package by NPM, you need to check the official website for duplicate names before publishing it.
  4. After successful publishing, you can install dependencies by executing NPM install llzscc_cli.