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:
- create folder
- Create Files and Entry Files
- Create a package.json
- 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
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
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
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