Hands have a front-end scaffold of their own


Many little buddies have been struggling about what scaffolding is?In fact, the core function is to create the project initial file, the problem comes again, is there not enough scaffolding on the market, why do you want to write it yourself?

As soon as you mention scaffolding, you'll think vue-cli, create-react-app, dva-cli.... Their characteristics are, not to mention, exclusive!But in the development of the company, you will find the following series of problems!

  • Business Types
  • Multiple wheels, project upgrades, etc.
  • Company code specification, not uniform

Before you develop your own cli, you must first see how some great CLIS are implemented!Although not the first to eat crabs, think about how you can eat better

1. Prerequisite modules

Let's start with the well-known vue-cli and see which npm packages he uses to implement it

  • commander: parameter parsing--help actually helped him~
  • inquirer: an interactive command-line tool that enables command-line selection
  • download-git-repo: download templates from Git
  • chalk: chalk helps us draw a variety of colors in the console
  • Metasmith: Read all files for template rendering
  • consolidate: unified template engine

Imagine what you want to do first:

Initialize project quick-cli create project-name from template
Initialization profile quick-cli config set repo-name

2. Project Creation

Say nothing more. We started creating projects and writing our own scaffolds~~

npm init -y # Initialize package.json
npm install eslint husky --save-dev # eslint is responsible for code validation and husky provides git hook functionality
npx eslint --init # Initialize eslint configuration file

2.1 Create folders

├── bin
│   └── www  // Root file for global command execution
├── package.json
├── src
│   ├── main.js // Entry File
│   └── utils   // Storage Tool Method
│── .huskyrc    // git hook
│── .eslintrc.json // Code Specification Checks

2.2 eslint configuration

Configure the code under the package.json check src folder

"scripts": {
    "lint":"eslint src"
}

2.3 Configure husky

Check code compliance before git submission

{
  "hooks": {
    "pre-commit": "npm run lint"
  }
}

2.4 Link Global Package

Sets the WW file in the bin directory to be called when quick-cli is executed under the command

"bin": {
    "quick-cli": "./bin/www"
}

main is used as the entry file in the www file and executed in the node environment

#! /usr/bin/env node
require('../src/main.js');

Link packages to use globally

npm link

We have successfully used the quick-cli command on the command line and executed the main.js file!

3. Parse command line parameters

commander:The complete solution for node.js command-line interfaces

Blow a commander first. The commander can automatically generate help and parse option parameters!

Vue-cli--help like this!
Vue-cli create <project-namne>

3.1 Use commander

npm install commander

main.js is our entry file

const program = require('commander');

program.version('0.0.1')
  .parse(process.argv); // process.argv is the parameter that the user passes in from the command line

Execute quick-cli--help already has a hint!

This version number should be the version number of the current cli project, we need to get it dynamically, and for convenience we put all the constants in the constants folder under util

const { name, version } = require('../../package.json');

module.exports = {
  name,
  version,
};

So we can get the version number dynamically

const program = require('commander');

const { version } = require('./utils/constants');

program.version(version)
  .parse(process.argv);

3.2 Configuration Instruction Commands

Traverse to generate the corresponding command, executing the action based on the functional configuration we want to achieve

const actionsMap = {
  create: { // Create Template
    description: 'create project',
    alias: 'cr',
    examples: [
      'quick-cli create <template-name>',
    ],
  },
  config: { // Configuration Profile
    description: 'config info',
    alias: 'c',
    examples: [
      'quick-cli config get <k>',
      'quick-cli config set <k> <v>',
    ],
  },
  '*': {
    description: 'command not found',
  },
};
// Loop Creation Command
Object.keys(actionsMap).forEach((action) => {
  program
    .command(action) // Name of the command
    .alias(actionsMap[action].alias) // Alias of command
    .description(actionsMap[action].description) // Description of the command
    .action(() => { // action
      console.log(action);
    });
});

program.version(version)
  .parse(process.argv);

3.3 Write help commands

Listen for help commands to print help information

program.on('--help', () => {
  console.log('Examples');
  Object.keys(actionsMap).forEach((action) => {
    (actionsMap[action].examples || []).forEach((example) => {
      console.log(`${example}`);
    });
  });
});

Now that we've configured the command line really well, let's get started!

4.create command

The main function of the create command is to pull the template from the git repository and download the corresponding version locally. If there are templates, they will be rendered according to the information the user fills in and generated to the directory where the command is currently running~

action(() => { // action
  if (action === '*') { // If the action does not match the description, the input is incorrect
    console.log(acitonMap[action].description);
  } else { // Pass in parameters by referencing the corresponding action file
    require(path.resolve(__dirname, action))(...process.argv.slice(3));
  }
}

Introduce files of corresponding modules dynamically according to different actions

Create create.js

// Create Project
module.exports = async (projectName) => {
  console.log(projectName);
};

Execute quick-cli create project to print out the project

4.1 Pull Items

We need to get all the template information in the warehouse. My templates are all on git. Here, for example, git, I use axios to get relevant information to ~~

npm i axios

Here with the help of github's api

const axios = require('axios');
// 1). Get a list of warehouses
const fetchRepoList = async () => {
  // Get all the warehouse information in the current organization, where project templates are stored
  const { data } = await axios.get('https://api.github.com/orgs/quick-cli/repos');
  return data;
};

module.exports = async (projectName) => {
  let repos = await fetchRepoList();
  repos = repos.map((item) => item.name);
  console.log(repos);
};

I found that I had a very bad experience during installation without any hints, and the end result I hope is user-friendly!

4.2 inquirer & ora

Let's solve the problems mentioned above

npm i inquirer ora 
module.exports = async (projectName) => {
  const spinner = ora('fetching repo list');
  spinner.start(); // Start loading
  let repos = await fetchRepoList();
  spinner.succeed(); // End loading

  // Select Template
  repos = repos.map((item) => item.name);
  const { repo } = await Inquirer.prompt({
    name: 'repo',
    type: 'list',
    message: 'please choice repo template to create project',
    choices: repos, // Selection mode
  });
  console.log(repo);
};

The command line choices we see are basically based on inquirer s, which allow different ways of asking questions.

4.3 Getting Version Information

Like getting templates, we can do it again

const fetchTagList = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/quick-cli/${repo}/tags`);
  return data;
};
// Get Version Information
spinner = ora('fetching repo tags');
spinner.start();
let tags = await fetchTagList(repo);
spinner.succeed(); // End loading

// Select Version
tags = tags.map((item) => item.name);
const { tag } = await Inquirer.prompt({
  name: 'tag',
  type: 'list',
  message: 'please choice repo template to create project',
  choices: tags,
});

We find that every time we need to turn loading on and off, duplicate code can't be let go of it!Let's just wrap it up:

const wrapFetchAddLoding = (fn, message) => async (...args) => {
  const spinner = ora(message);
  spinner.start(); // Start loading
  const r = await fn(...args);
  spinner.succeed(); // End loading
  return r;
};
// It's more comfortable to use this time.
let repos = await wrapFetchAddLoding(fetchRepoList, 'fetching repo list')();
let tags = await wrapFetchAddLoding(fetchTagList, 'fetching tag list')(repo);

4.4 Download Project

We have successfully obtained the project template name and the corresponding version, so we can download it directly!

npm i download-git-repo

Unfortunately, this is not a promise method. It's OK to wrap it up by ourselves:

const { promisify } = require('util');
const downLoadGit = require('download-git-repo');
downLoadGit = promisify(downLoadGit);

node has provided you with a ready-made way to quickly convert an asynchronous api into a promise

Find a temporary directory to store the downloaded files before downloading. Let's continue configuring constants:

const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`;

Here we download the file to the.template file under the current user. Because the different directories of the system are obtained differently, process.platform gets win32 under windows. Here is mac, so the value obtained is darwin, and then the user directory is obtained according to the corresponding environment variables.

const download = async (repo, tag) => {
  let api = `quick-cli/${repo}`; // Download Project
  if (tag) {
    api += `#${tag}`;
  }
  const dest = `${downloadDirectory}/${repo}`; // Download the template to the appropriate directory
  await downLoadGit(api, dest);
  return dest; // Return to download directory
};


// Download Project
const target = await wrapFetchAddLoding(download, 'download template')(repo, tag);

For simple projects, you can copy the downloaded project directly to the directory where the command is currently executed.

Installing ncp allows you to copy files:

npm i ncp

Like this:

let ncp = require('ncp'); 
ncp = promisify(ncp);
// Copy the downloaded file to the directory where the command is currently executed
await ncp(target, path.join(path.resolve(), projectName));

Of course, there is more rigorous work to do here to determine if there are duplicate files in the current directory, etc. There are also many details to consider, such as whether to create a project multiple times using the downloaded templates, so that everyone can use them freely.

4.5 Template Compilation

Just now I said a simple file, of course a direct copy would be fine, but sometimes users can customize the contents of the download template. Take the package.json file for example, users can name items, set descriptions, etc. as prompted.

Here I added it to the project template ask.js

module.exports = [
    {
      type: 'confirm',
      name: 'private',
      message: 'ths resgistery is private?',
    },
    ...
]

Generate the final package.json based on the corresponding query

The ejs template is used in the downloaded template

{
  "name": "vue-template",
  "version": "0.1.2",
  "private": "<%=private%>",
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "vue": "^2.6.10"
  },
  "autor":"<%=author%>",
  "description": "<%=description%>",
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "vue-template-compiler": "^2.6.10"
  },
  "license": "<%=license%>"
}

You should have thought of it when you write here!The core principle is to download the template file, iterate through the rendering templates according to the information the user fills in, and copy the rendered results to the directory where the command is executed

Install required modules

npm i metalsmith ejs consolidate
const MetalSmith = require('metalsmith'); // traverse folder
let { render } = require('consolidate').ejs;
render = promisify(render); // Packaging Rendering Method

// No ask file description required compilation
if (!fs.existsSync(path.join(target, 'ask.js'))) {
  await ncp(target, path.join(path.resolve(), projectName));
} else {
  await new Promise((resovle, reject) => {
    MetalSmith(__dirname)
      .source(target) // Traverse downloaded directories
      .destination(path.join(path.resolve(), projectName)) // Output rendered results
      .use(async (files, metal, done) => {
        // Boxes Ask Users
        const result = await Inquirer.prompt(require(path.join(target, 'ask.js')));
        const data = metal.metadata();
        Object.assign(data, result); // Put the results of the query in metadata to ensure they are available in the next Middleware
        delete files['ask.js'];
        done();
      })
      .use((files, metal, done) => {
        Reflect.ownKeys(files).forEach(async (file) => {
          let content = files[file].contents.toString(); // Get the contents of the file
          if (file.includes('.js') || file.includes('.json')) { // Templates are only possible if they are js or json
            if (content.includes('<%')) { // File uses <% before I need to compile
              content = await render(content, metal.metadata()); // Rendering templates with data
              files[file].contents = Buffer.from(content); // Rendered results can be replaced
            }
          }
        });
        done();
      })
      .build((err) => { // Execution Middleware
        if (!err) {
          resovle();
        } else {
          reject();
        }
      });
  });
}

The logic here is to implement template replacement as described above, and the functionality of this installation project is complete!We found that all the address paths used here were written to death, but we hope this is a more general scaffold that allows users to configure the pull address themselves to

5.config command

The main purpose of creating a new config.js is actually to read and write the configuration file. Of course, if the configuration file does not exist, you need to provide a default value. Let's write the constants first:

constants.jsConfiguration

const configFile = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.quickrc`; // Storage location of configuration files
const defaultConfig = {
  repo: 'quick-cli', // Default pulled repository name
};

Write config.js

const fs = require('fs');
const { defaultConfig, configFile } = require('./util/constants');
module.exports = (action, k, v) => {
  if (action === 'get') {
    console.log('Obtain');
  } else if (action === 'set') {
    console.log('Set up');
  }
  // ...
};

Configuration files of general rc types are in ini format, that is:

repo=quick-cli
register=github

Download ini module parsing profile

npm i ini

The code here is simple, just file manipulation:

const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');

const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');

module.exports = (action, k, v) => {
  const flag = fs.existsSync(configFile);
  const obj = {};
  if (flag) { // Profile Exists
    const content = fs.readFileSync(configFile, 'utf8');
    const c = decode(content); // Parse file into object
    Object.assign(obj, c);
  }
  if (action === 'get') {
    console.log(obj[k] || defaultConfig[k]);
  } else if (action === 'set') {
    obj[k] = v;
    fs.writeFileSync(configFile, encode(obj)); // Write content to ini format into string
    console.log(`${k}=${v}`);
  } else if (action === 'getVal') { 
    return obj[k];
  }
};

getVal This method is used to get the configuration variable when the create command is executed

const config = require('./config');
const repoUrl = config('getVal', 'repo');

So we can replace all quick-cli ps in the create method with the values we get!

The basic core of this approach is ok!The rest of you can expand on your own!

6. Project Publishing

Finally, to the final step, we pushed the project to npm, the process is no longer redundant!

nrm use npm
npm publish # Successfully posted ~~

Installation can be done with NPM install quick-cli-g!

Hope you can get something out of this article!
If you think the article is good, please follow the WeChat Public Number and continue to get more content!

Keywords: node.js npm Vue git JSON

Added by tidalwave on Mon, 02 Sep 2019 04:08:50 +0300