Hand-on instructions for writing CLIS using nodejs (command line)

Posted by anindya23 on Sat, 01 Jan 2022 13:50:47 +0100

Why insist on writing? Writing is the process of seeking answers.

In front-end development, we often use cli tools such as webpack-cli, Vue-cli, create-react-app. In actual business development, we also have a lot of CLI requirements to help us achieve the rapid creation of new projects or modules. Let's get started!

Let's demonstrate cli with a demo. This demo is to quickly build a web pack base project by typing my-cli, execute npm run start or npx webpack serve r start service after entering the project created by cli


Target

Create project hello-world through my-cli

// hello-world
├── src
│   └── index.js
├── index.html
├── .gitignore
├── package.json
├── package-lock.json
└── webpack.config.js

Execute npm run start service under hello-world project, output hello-world!

1. Initialize cli project

Create a new project, call it my-cli, and use the esm module of nodejs

The esm module does not support automatic parsing of file extensions and the ability to import directories with indexed files. Be careful not to lose the index later. js,. js

package.json

{
  ...
  "type": "module",
  ...
}

Project Directory

my-cli
├── bin
│   ├── prompt/                        # Interactive commands
│   ├── template                       # ejs template
│   │   ├── main                 		   # Entry File Template
│   │   		├── main.ejs               # Entry File Template ejs
│   │   		└── index.js               # Export Template
│   ├── utils/                         # Tool Functions
│   └── index.js                       # CLI Execution Entry File
├── .gitignore
├── package.json
└── package-lock.json

2. Cli implementation ideas

The essence of cli is to run a node script

// bin/index.js
console.log('hello world!')

Command line:

## input
> node bin/index.js

## output
> hello world

Executing the above command will output hello world at the command line!

cli needs the following steps to accomplish its goals:

  1. create folder
  2. Create Files and Entry Files
  3. Create a package.json
  4. Installation Dependency

2.1 Create folders

Creating folders through node's fs module

import fs from 'fs'

fs.mkdirSync('./helloWorld')
fs.mkdirSync('./helloWorld/src')

Execute node bin/index.js will see an additional helloWorld folder and an src folder added

Above. / helloWorld is essentially the file root path we created by cli, so let's extract the root path and modify the code above

import fs from 'fs'

const getRootPath = () => {
  return './helloWorld'
}

fs.mkdirSync(getRootPath())
fs.mkdirSync(`${getRootPath()}/src`)

2.2 Create Files and Entry Files

Or create files through node's fs module

File write method fs.writeFile()

fs.writeFile('File Path','What to Write', ['Encoding'],'Callback Function');

fs.writeFileSync(`${getRootPath()}/src/index.js`, 'indexjs')
fs.writeFileSync(`${getRootPath()}/index.html`, 'indexhtml')
fs.writeFileSync(`${getRootPath()}/webpack.config.js`, 'webpack')

Execute node bin/index again. JS will error because the helloWorld folder already exists

Repair add an npm script for quick testing

package.json

{
  ...
  "test": "rm -rf ./helloWorld && node bin/index.js"
  ...
}

Executing npm run test will add a helloWorld/src/index.js, and the file content already exists

2.3 Create a package.json

fs.writeFileSync(`${getRootPath()}/src/package.json`, 'package')

Executing npm run test will add a helloWorld/src/package.json, and the file content already exists

2.4 Installation Dependency

This step we'll implement later

3. ejs template file content

3.1 Install ejs

ejs document

npm i ejs

3.2 ejs Example

<% if (user) { %>
  <h2><%= user.name %></h2>
<% } %>

3.3 Getting absolute paths

bin/utils/index.js

import path from 'path'
import { fileURLToPath } from 'url'
// Get absolute path
export const getRootPath = (pathUrl) => {
  // esm module does not have CommonJS_u dirname
  // Here you need to encapsulate a u with the tool function fileURLToPath dirname
  // Note: here_u dirname points to the current file bin/utils/index.js path, so add two layers. / To modify the path to the bin directory
  const __dirname = fileURLToPath(import.meta.url)
  return path.resolve(__dirname, `../../${pathUrl}`)
}

3.4 Using ejs templates

template directory structure

├── bin
    ├── template                      # ejs template
        ├── indexHtml                 # html
        │		├── indexHtml.ejs
        │		└── index.js
        ├── main                 		  # Entry File
        │		├── main.ejs
        │		└── index.js
        ├── package                 	# package.json
        │		├── package.ejs
        │		└── index.js
        └── webpackConfig             # webpack.config.js
        		├── webpackConfig.ejs
         		└── index.js    

bin/template/indexHtml/indexHtml.ejs

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>test-<%= packageName %></title>
</head>
<body>

</body>
</html>

bin/template/indexHtml/index.js

import ejs from 'ejs'
import fs from 'fs'
import { getRootPath } from "../../utils/index.js";

export default ({ packageName }) => {
  // Use fs.readFileSync Reads Files
  const file = fs.readFileSync(getRootPath('template/indexHtml/indexHtml.ejs'))
  // Use ejs.render processing. EJS template file, passing in variables
  return ejs.render(file.toString(), { packageName })
}

bin/template/main/main.ejs

const h1 = document.createElement('h1');
h1.innerText = 'Hello World!';
document.body.appendChild(h1);

bin/template/main/index.js

import ejs from 'ejs'
import fs from 'fs'
import { getRootPath } from "../../utils/index.js";

export default () => {
  const file = fs.readFileSync(getRootPath('template/main/main.ejs'))
  return ejs.render(file.toString(), {})
}

bin/template/package/package.ejs

{
  "name": "<%= packageName %>",
  "version": "1.0.0",
  "description": "<%= packageName %>...",
  "main": "index.js",
  "scripts": {
    "start": "npx webpack serve",
    "build": "npm webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.2"
  }
}

bin/template/package/index.js

import ejs from 'ejs'
import fs from 'fs'
import { getRootPath } from "../../utils/index.js";

export default ({ packageName }) => {
  const file = fs.readFileSync(getRootPath('template/package/package.ejs'))
  return ejs.render(file.toString(), { packageName })
}

bin/template/webpackConfig/webpackConfig.ejs

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    clean: true
  },
  devtool: '<%= devtool %>',
  devServer: {
    port: <%= port %>,
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

bin/template/webpackConfig/index.js

import ejs from 'ejs'
import fs from 'fs'
import { getRootPath } from "../../utils/index.js";

export default ({ devtool, port }) => {
  const file = fs.readFileSync(getRootPath('template/webpackConfig/webpackConfig.ejs'))
  return ejs.render(file.toString(), { devtool, port })
}

3.5 Loading Template Content

bin/index.js

import fs from 'fs'

import indexHtml from './template/indexHtml/index.js'
import main from './template/main/index.js'
import packageJson from './template/package/index.js'
import webpackConfig from './template/webpackConfig/index.js'

// Created Project Path
const getProjectPath = () => {
  return './helloWorld'
}

const config = {
  packageName: 'helloWorld',
  port: 8080,
  devtool: 'eval-cheap-module-source-map',
}

// 1. Create folders
fs.mkdirSync(getProjectPath())
fs.mkdirSync(`${getProjectPath()}/src`)

// 2. Create files and entry files
fs.writeFileSync(`${getProjectPath()}/src/index.js`, main())
fs.writeFileSync(`${getProjectPath()}/index.html`, indexHtml(config))
fs.writeFileSync(`${getProjectPath()}/webpack.config.js`, webpackConfig(config))

// 3. Create a package.json
fs.writeFileSync(`${getProjectPath()}/package.json`, packageJson(config))

Executing npm run test, you can see the contents of the file and the generation using the ejs template, and the dynamic content you configured has been filled in, but the content is not formatted, which we will work on later

4. inquirer interactive command line

inquirer document

Install inquirer

npm i inquirer

Create Q&A Interactive Command

template directory structure

├── bin
    ├── prompt                        # Q&A
        ├── index.js                  # Entrance
        ├── packageName.js            # entry name
        ├── devtool                 	# source map
        ├── port                 	    # Port number
        └── installTool               # yarn / npm

prompt/packageName.js

export default () => ({
  type: 'input', // Command Type
  name: 'packageName', // Key Name
  message: 'set package name', // Prompt
  validate(val) { // check
    if (val) return true;
    return 'Please enter package name'
  }
})

prompt/devtool.js

export default () => ({
  type: 'list',
  name: 'devtool',
  message: 'set devtool',
  default: 'eval-cheap-module-source-map', // Default value
  choices: [
    { value: 'eval-cheap-module-source-map', name: 'eval-cheap-module-source-map' },
    { value: 'eval-cheap-source-map', name: 'eval-cheap-source-map' },
    { value: 'eval-source-map', name: 'eval-source-map' },
  ]
})

prompt/port.js

export default () => ({
  type: 'input',
  name: 'port',
  default: 8080,
  message: 'set port number',
})

prompt/installTool.js

export default () => ({
  type: 'list',
  name: 'installTool',
  message: 'select installation Tool',
  default: 'yarn',
  choices: [
    { name: 'yarn' },
    { name: 'npm' }
  ]
})

prompt/index.js

import inquirer from 'inquirer'
import packageName from "./packageName.js";
import port from "./port.js";
import devtool from "./devtool.js";
import installTool from "./installTool.js";

export default async () => inquirer.prompt([
  packageName(),
  port(),
  devtool(),
  installTool(),
])

**bin/index.js ** Introducing interactive commands

// ...
import question from './prompt/index.js'

const config = await question()

// Created Project Path
const getProjectPath = () => {
  // Dynamic Project Name
  return `./${ config.packageName }`
}
// ...

Execute npm run test

When the execution is complete, you can see that the project has been created successfully!

5. Exca Installation Dependency

execa document

install

npm i execa
// Installation Dependency
// The first parameter is a string, which is the command you typed when running the script in cmd.
// The second parameter is a list of parameters. Place the parameters you want to pass in to the script. Note that it is a list and will not return without a list.
// The third parameter, a function, returns an object
execa(config.installTool, ['install'], {
  cwd: getProjectPath(), // Root Path
  stdio: [2, 2, 2] // Input and output streams of child processes inherit from parent processes, displaying inputs and outputs of child processes in the current parent process
})

Execute npm run test, you can see that you can install dependencies!

Enter the project you created and execute NPM run server to see that the service started successfully!

! [Insert picture description here] ( https://img-blog.csdnimg.cn/862fffa05d5647b9935fac7d27d5cc5c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LqU6JmO5oiY55S75oif,size_20,color_FFFFFF,t_70,g_se,x_16 3 -)

6. Use

6.1 bin

In package.json adds a bin field.

The bin field can be written directly to the executed file or as a key-value pair to define the script command to execute

{
  ...
  "name": "my-cli",
  "bin": "bin/index.js",
  // or
  "bin": {
    // Here you can define it as you like
    "my-cli": "bin/index.js",
  },
  ...
}

The bin field tells npm that the js script pointed to by the bin field can be executed from the command line and called with my-cli's command. Of course, the name of the command line is your freedom to write anything you want, such as:

{
  ...
  "bin": {
    "create-app": "bin/index.js",
  },
  ...
}

6.2 Declare the execution environment

In bin/index.js file top declaration execution environment

#!/usr/bin/env node

// ...

Add #!/ usr/bin/env node or #!/ usr/bin/node, which tells the system that the following script is executed using nodejs. Of course, this system does not include windows, because there is a legacy of JScript under windows that will make your script run away.

  • #!/ usr/bin/env node means to let the system find the execution program of the node itself.
  • #!/ Usr/bin/node means explicitly informing the system that the node executes in the path / usr/bin/node.

6.1 Test Phase

Executing npm link in the cli root directory links the cli module to the global npm module, similar to npm i-g, but he can debug the code because npm link can be understood as a module link or shortcut.

6.2 Formal Stage

Publish the CLI project to npm through npm publish, install it globally as npm i-g, and execute my-cli to execute our cli tool.

npm detected package. There is a bin field in JSON that generates an executable in the global npm package directory at the same time, and when we execute my-cli directly from the system command line, these scripts are actually executed. Because when node is installed, npm configures this directory as a system variable environment. When you execute a command, the system first looks for the system command and system variable, then looks for the command name in the variable environment, finds the directory, finds the executable that matches the command name, and executes it directly. This is how vue-cli and webpack-cli are executed.

At this point, our first cli script has been successfully installed. You can type your cli name directly from the command line to output the desired result quickly and successfully!!

However, our CLI tools seem to be a little bit more complete

7. Perfect

7.1 Template for formatting ejs output

As mentioned earlier, ejs output file templates may not have formatted content, which makes us uncomfortable to read. We can format them in many ways, such as using IDE manually, ESLint, stylelink, or using Prettier . Here's a demonstration of prettier

install

npm i prettier

Use, check the documentation for more options Prettier

bin/template/indexHtml.index.js

import ejs from 'ejs'
import fs from 'fs'
import prettier from "prettier";
import { getRootPath } from "../../utils/index.js";

export default ({ packageName }) => {
  const file = fs.readFileSync(getRootPath('template/indexHtml/indexHtml.ejs'))
  const code = ejs.render(file.toString(), { packageName })
  // Format
  return prettier.format(code, { parser: 'html' })
}

bin/template/main.index.js

import ejs from 'ejs'
import fs from 'fs'
import prettier from 'prettier'
import { getRootPath } from "../../utils/index.js";

export default () => {
  const file = fs.readFileSync(getRootPath('template/main/main.ejs'))
  const code = ejs.render(file.toString(), {})
  // Format
  return prettier.format(code, { parser: 'babel' })
}

bin/template/package.index.js

import ejs from 'ejs'
import fs from 'fs'
import prettier from "prettier";
import { getRootPath } from "../../utils/index.js";

export default ({ packageName }) => {
  const file = fs.readFileSync(getRootPath('template/package/package.ejs'))
  const code = ejs.render(file.toString(), { packageName })
  // Format
  return prettier.format(code, { parser: 'json' })
}

bin/template/webpackConfig.index.js

import ejs from 'ejs'
import fs from 'fs'
import prettier from 'prettier'
import { getRootPath } from "../../utils/index.js";

export default ({ devtool, port }) => {
  const file = fs.readFileSync(getRootPath('template/webpackConfig/webpackConfig.ejs'))
  const code = ejs.render(file.toString(), { devtool, port })
  // Format
  return prettier.format(code, { parser: 'babel' })
}

7.2 Beautification process output

npm i chalk
import chalk from 'chalk'

console.log(chalk.blue(`create folder -> ${getProjectPath()}`));
console.log(chalk.blue(`Create Files and Entry Files -> index.js`));
console.log(chalk.blue(`Establish package.json`));
console.log(chalk.blue(`Start Installing Dependencies`));

7.3 Operation git

At present, there is no git for the project created by CLI, so let's perfect it

import { execa } from 'execa';
// 1. Create folders
// 2. Create files and entry files
// 3. Create a package.json
// 4. Installation Dependency
// 5. Operating git
await execa(`git`, ['init'], { cwd: getProjectPath(), })
await execa(`git`, ['add', './'], { cwd: getProjectPath(), })
await execa(`git`, ['commit', '-m', 'init'], { cwd: getProjectPath(), })

This is the end of the tutorial, but you and I are still on the way

Other cli lessons will follow

Topics: node.js Front-end npm cli