preface

If you use the Vue stack, you must have used @vue/ CLI. If you use the React stack, you must know create-React-app. Using these tools can achieve a line of command to generate the code template we want, greatly facilitate our daily development, let the computer to do the tedious work, and we can save a lot of time for learning, communication, development.

The function of CLI tool is that it can solve the problem that we often need to do repeatedly in the development process with one line of code. For example, when we write requirements, we need to add the initialization code of the page for each new page, while the initialization code of the same file type is often the same, such as example.vue. At the same time, we need to add the corresponding route, for example, add the corresponding routing rule in router.js. It’s all tedious and repetitive. Do you do it all over again every time? It’s time to make a change, write your own CLI tool, one line command, 3 seconds into coding state!

In this article, you will learn how to develop a CLI project from scratch and how to publish your own packages using NPM, using your own FC-VUe-CLI as an example.

Put the project address in advance

Source code address: source code

NPM address: NPM

Original address on Github:

github

The functionality to be implemented

Fc-vue add-page You can run the fc-vue add-page command to add a template file for a page, eliminating the need to manually create a file, copy the initialization code, and add the corresponding route configurationCopy the code

The scaffold name is fC-vue, which is defined by the name field in package.json.

The directory structure

The entrance (bin/index. Js)

The entry file does only one thing, and that is to determine if the current node version is greater than 10 and to remind the user to upgrade the node if the version number is less than 10Copy the code
#! /usr/bin/env node // 'use strict'; const chalk = require('chalk'); const currentNodeVersion = process.versions.node; const major = currentNodeVersion.split('.')[0]; if (major < 10) { console.error( chalk.red( `You are running Node \n${currentNodeVersion} \nvue-assist-cli requires Node  10 or higher.\nPlease update your version of Node` ) ); process.exit(1); } require('.. /packages/init');Copy the code

Initialization commands (Packages /init.js)

This is where you initialize the command you want to implement. For example, if I want to implement add-Page, I’ll use the Commander library.

const { program } = require('commander'); const { log } = require('./lib/util'); Program.version (require('.. /package.json').version); [name] specifies that this parameter is optional. We want to be compatible with different usage methods, so this parameter is not optional. //.option is used to add optional parameters. //.action is used to respond to user input. Command ('add-page [name]').description('add a page, / SRC /pages or./ SRC /page by default./ SRC /pages or./ SRC /page, and add route \n support "/" to create subdirectories. Option ('-s, --simple', 'create a simple version of the page, A new one. Only the vue file '). Option (' - t - the title < title > ', 'the page title). The action (the require ('/commands/add - page')). On (' -- help ', () => {log(' support fc-vue add-page to select input information '); }); Parse (process.argv); // Format the command-line arguments program.parse(process.argv);Copy the code

Handling user input commands (Packages /commands/add-page.js)

There are several libraries to use, shelljs for shell commands, we for manipulating files, and Chalk for styling printouts. The function retrieves user input by name,cmdObj, where name is the name in. Command (‘add-page [name]’) and other parameters in the cmdObj object

const fs = require('fs'); const shell = require('shelljs'); const chalk = require('chalk'); const { askQuestions, askCss } = require('.. /lib/ask-page'); const checkContext = require('.. /lib/checkContext'); const copyTemplate = require('.. /lib/copy-template'); const addRouter = require('.. /lib/add-router'); const { error, log, success } = require('.. /lib/util'); shell.config.fatal = true; Module.exports = async (name, cmdObj) => {try {// default to use less, let cssType = 'less'; let simple = cmdObj.simple; let title = cmdObj.title; if (! The name && (simple | | title)) {error (' bad command, the lack of the name of the page). process.exit(1); } // If the user does not enter name, [fc-vue add-page] enters the q&A mode, and obtains the user's input if (! name) { const answers = await askQuestions(); // console.log(answers); name = answers.FILENAME; title = answers.TITLE; simple = answers.SIMPLE; if (! simple) { const res = await askCss(); cssType = res.CSS_TYPE; // console.log(process.cwd()); // console.log(process.cwd()); Let {destDir, destDirRootName, rootDir} = checkContext(name, cmdObj, 'page'); // copyTemplate to target file let {destFile} = copyTemplate(destDir, simple, cssType); if (fs.existsSync(destFile)) { await addRouter(name, rootDir, simple, destDirRootName, title); Log (' successfully created ${name}, please check under ${destDir} '); } else {console.error(' creation failed, please go to project [root] or [@src] '); } } catch (error) { console.error(chalk.red(error)); Console. error(' failed to create page, please make sure to do this operation \n in project [root] or [@src] directory, otherwise please contact @zhongyi '); }};Copy the code

Q&a mode (Packages /lib/ask-page.js)

You need the Inquirer here. This is pretty simple, basically just an array of what you want the user to input, and each interaction can be input, list, and so on. With the user input we get here we can call packages/commands/add-page.js and get these parameters.

const inquirer = require('inquirer'); const askQuestions = () => { const questions = [ { name: 'FILENAME', type: 'input', message: }, {name: 'TITLE', type: 'input', message: 'please enter the TITLE of the page (meta. TITLE) ',}, {type: 'list', name: 'SIMPLE', message: 'What is the template type?', choices: ['normal: [create.vue. Function (val) {return val. Split (':')[0] === 'simple'? True: false;},},]; return inquirer.prompt(questions); };Copy the code

Check the environment when users execute commands (packages/lib/checkContext js)

Since we’re not sure if the user will use it the way we expect, we add some judgment here to make sure the user is behaving properly, or throw an error telling the user how to use it. The main thing is to ensure that the user executes the command in the project root or SRC directory path. Then you have to verify that the directory structure of the user’s project conforms to the specifications we provide (and basically, the community’s specifications). Finally, of course, determine whether the page you want to add already exists.

const fs = require('fs'); const path = require('path'); const { error } = require('./util'); /** * check whether the user executes in the project root directory or./ SRC directory, and whether there is an agreed project directory structure, Whether the component already exists * @param {Stirng} name * @param {Object} cmdObj * @return {Object} {destDirRootName,destDir,rootDir} Destination folder name, */ const checkContext = (name, cmdObj, type) => {// console.log(process.cwd()); let destDir, destDirRoot, destDirRootName; const curDir = path.resolve('.'); let rootDir = '.'; const basename = path.basename(curDir); If (basename === 'SRC ') {rootDir = path.resolve('.. ', rootDir); } // Check whether there is a SRC directory under the project root directory rootDir. If there is no SRC directory, the user is not running the command in the correct path. Fs.existssync (path.join(rootDir, 'SRC ')) {error(' failed to create page, please go to project [root] or [@src] '); process.exit(1); } // -c if (type === 'component') {// Create a component. SRC /components SRC/Component If (fs.existssync (path.resolve(rootDir, 'src/components'))) { destDir = path.resolve(rootDir, 'src/components', name); } else if (fs.existsSync(path.resolve(rootDir, 'src/component'))) { destDir = path.resolve(rootDir, 'src/component', name); } else {error(' Your generic component directory does not conform to the specification, please put it under/SRC /components '); SRC /views SRC /pages SRC /page If (fs.existssync (path.resolve(rootDir, 'src/views'))) { destDir = path.resolve(rootDir, 'src/views', name); destDirRootName = 'views'; } else if (fs.existsSync(path.resolve(rootDir, 'src/pages'))) { destDir = path.resolve(rootDir, 'src/pages', name); destDirRootName = 'pages'; } else if (fs.existsSync(path.resolve(rootDir, 'src/page'))) { destDir = path.resolve(rootDir, 'src/page', name); destDirRootName = 'page'; } else {error(' Your page component directory does not meet the specification, please put it in/SRC /view or/SRC /pages or/SRC /page '); }} / / whether the existing component if ((cmdObj. Simple && fs. ExistsSync (destDir + 'vue')) | | (! Cmdobj.simple && fs.existssync (destDir + '/index.vue')) {error(' ${name} page/component already exists, failed to create! `); process.exit(1); } return { destDirRootName, destDir, rootDir }; }; module.exports = checkContext;Copy the code

Copy templates to target path (packages/lib/copy-template.js)

Once the context is confirmed and the user’s input parameters are in hand, we can happily add pages by copying the template we have prepared into the target file. In this case, we need to consider whether the user selects the type normal or simple to add different page templates according to different types. Of course, it also supports LESS, SCSS, etc. For example, when the user runs fc-vue add-page user/login –title= login page, the initial template file will be created under SRC /views/user/login, including.js.vue.less

const shell = require('shelljs'); const path = require('path'); shell.config.fatal = true; /** ** @param {String} destDir Target file path * @param {Boolean} simple * @param {less, SCSS,sass,stylus} cssType * @return { */ const copyTemplate = (destDir, simple, cssType) => {let sourceDir, destFile; // -s if (simple) {// Create a simple.vue file sourceDir = path.resolve(__dirname, '.. /.. /template/vue-page-simple-template.vue' ); shell.mkdir('-p', destDir.slice(0, destDir.lastIndexOf('/'))); destDir += '.vue'; shell.cp('-R', sourceDir, destDir); destFile = destDir; } else { shell.mkdir('-p', destDir); sourceDir = path.resolve( __dirname, `.. /.. /template/vue-page-template-${cssType}/*` ); shell.cp('-R', sourceDir, destDir); destFile = path.resolve(destDir, 'index.vue'); } return { sourceDir, destFile }; }; module.exports = copyTemplate;Copy the code

Adding a route (package/lib/add-router.js)

We want to automatically configure routing when we add the page template. Router.js and insert the route that the user added to the page. We agree that the components under the SRC /views directory are page-level, that is, /user/login/index.vue corresponds to /user/login. SRC /router/index.js SRC /router/index.js SRC /router/index.js SRC /router/index.js

import Vue from 'vue'; import VueRouter from 'vue-router'; import Home from '.. /views/Home.vue'; Vue.use(VueRouter); Const routes = [****** ***** {path: '/user/login', name: 'user/login', meta: {title: 'login page'}, Component: () => import(/* webpackChunkName: "user/login" */ './views/user/login/index.vue'), } ]; const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, }); export default router;Copy the code

Back to the implementation of adding routing configurations, packages/lib/add-router.js.

const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); /** ** @param {String} name Page name * @param {String} rootDir Project directory * @param {Boolean} simple Simple mode * @param {String} Pages views Page * @param {String} title page */ const addRouter = async (name, rootDir, simple, destDirRootName, title) => { let routerPath, pagePath; if (fs.existsSync(path.resolve(rootDir, './src/router.js'))) { routerPath = path.resolve(rootDir, './src/router.js'); } else if (fs.existsSync(path.resolve(rootDir, './src/router/index.js'))) { routerPath = path.resolve(rootDir, './src/router/index.js'); } else {error(' Your project routing file does not conform to the specification, please put it in/SRC /router.js or/SRC /router/index.js'); } pagePath = `./${destDirRootName}/${name}/index.vue`; if (simple) { pagePath = `./${destDirRootName}/${name}.vue`; } try { let content = await readFile(routerPath, 'utf-8'); // find const routes = and]; Between the content, namely routes array const reg = / const \ s + routes \ \ s * = ([\ s \ s] *) \] \ \ s *; /; const pathStr = `path: '/${name}',`; const nameStr = `name: '${name}',`; const metaStr = title ? `meta: { title: '${title}' },` : ''; let componentStr = `component: () => import(/* webpackChunkName: "${name}" */ '${pagePath}'),`; content = content.replace(reg, function (match, $1, index) { $1 = $1.trim(); if (! $1.endsWith(',')) { $1 += ','; } if (title) { return `const routes = ${$1} { ${pathStr} ${nameStr} ${metaStr} ${componentStr} } ]; `; } else { return `const routes = ${$1} { ${pathStr} ${nameStr} ${componentStr} } ]; `; }}); try { await writeFile(routerPath, content, 'utf-8'); } catch (err) { error(err); } } catch (err) { error(err); }}; module.exports = addRouter;Copy the code

Published to the NPM

The main thing is to configure the package.json file. Bin defines the NPM package entry.

"Name" : "fc - vue", "version" : "1.0.6," "bin" : {" fc - vue ":" bin/index. Js "},Copy the code
Run NPM login to login to NPM publish. The published version cannot be the sameCopy the code

Install and use

	$ npm i -g fc-vue
	$ fc-vue add-page
Copy the code

Using the demonstration

The end of the

That’s a simple FC-vue add-page, isn’t it?

Source code address: source code

NPM address: NPM