Preface: RECENTLY took over a project, construction period rush, demand is many. But for many requirements, the logical structure of the page is very similar. Just CV it and change it, but I didn’t want to be CV warrior and I thought, why not write a simple scaffold tool? In the future, a similar need can be quickly generated with a few lines of command on the console. Wouldn’t it be nice to spend the rest of your time studying?

So, I rolled up my sleeves and plus-one item for a final NPM package: YYds-CLI (one good item for uploading to NPM is one item that can be easily downloaded and used anywhere, and one item for my colleague’s sister-δΉ› plus-δΉ›).

The final implementation of scaffolding functions are as follows:

  1. Viewing version Information
  2. Viewing Upgrade Information
  3. Quickly generate template pages
  4. Template Path Configuration
  5. Check the configuration

Follow me to see how to implement the scaffolding tool.

The preparatory work

Start with preparation, create a new directory, and then initialize the project.

mkdir yyds-cli && cd yyds-cli && npm init -y
Copy the code

Then download the NPM package you need to use

  • fs-extraEnhanced Version of NodefsThe module
  • Chalk prints the log shader
  • Commander core package, Node command line registration tool
  • Figlet console interface prompt
  • Log-symbols Log symbols
  • Update-notifier Prompts the update tool
  • Minimist parsing parameter options
  • Inquirer command line interaction tool
  • Ejs rendering template engine
NPM I -- Save FS - Extra Chalk Commander Figlet [email protected] update- Notifier Minimist Inquirer EJSCopy the code

Note: Since packages will eventually be released to the NPM environment, we will install these dependencies in production. As for log-Symbols, we need to include the version number, because the latest version of the package will not run under node.

With all the preparation done, we began to write the first command.

Register the first commandyyds -v

First add a new field bin to package.json.

"bin": {
    "yyds": "./bin/index.js"
},
Copy the code

Then run the NPM link command to link the working directory to the global directory, so that you can run yyds directly in any directory.

Then create a bin directory in the current directory and create an index.js file that will hold all the instructions we want to register. The code for viewing the version information is as follows:

#! /usr/bin/env node
const program = require('commander')

program
  .version(require('.. /package.json').version, '-v, --version') // Read the version in package.json
  .usage('<command> [options]')


program.parse(process.argv)  // Parse the commands typed into the console
Copy the code

Note the first line #! /usr/bin/env node is required, otherwise node will fail to parse the corresponding directive

Now that we’ve done our first command, you can go to the console and type yyds -v to see what it looks like. Found out the following information, very chicken frozen?

All right, to calm your nerves, let’s move on to our second little feature: prompting for an upgrade.

Prompt to upgrade

When using other scaffolds or installing dependencies, we have encountered messages asking us to upgrade the version. How do they do this? Update notifier update notifier update notifier update notifier

In order to facilitate maintenance, we create a new lib folder in the working directory, and create a new update.js file in this directory to write the relevant code prompting the upgrade.

const updateNotifier = require('update-notifier');
const chalk = require('chalk')
const pkg = require('.. /package.json');

const notifier = updateNotifier({
    pkg,
    updateCheckInterval: 1000 // Check the frequency of updates, the official default is one day, here set to one second
});
async function update() {
  if (notifier.update) {
    console.log(`Update available: ${chalk.cyan(notifier.update.latest)}`);
    notifier.notify()
  } else {
    console.log('No new version available')}}module.exports = update
Copy the code

After writing the update logic, don’t forget to register in bin/index.js.

program
  .command('upgrade')
  .description('Check the yyds-cli version')
  .action(() = > {
    require(`.. /lib/update`)()
})
Copy the code

For testing, we temporarily went to package.json file and changed the name to vue-cli. Then we went to the console and hit YYds Upgrade. As you can see, it will prompt us to upgrade the new version.

After changing the name back to the original, press YYDS Upgrade, and you can see that the console prints No new version available.

Ok, the two instructions above are the appetizers, now we are going to finish the main function, which is the quick page generation I want to do.

Quick page generation

Achieve goals:

  1. Yyds G < path >Pages can be generated quickly
  2. If a file with the same name exists in the specified path, you need to ask whether to overwrite the file. Users can manually choose to overwrite the fileorDon’t cover
  3. take--forceParameter to forcibly overwrite a file
  4. through-tParameter can specify a template (to support multiple templates)
  5. Transfer and check

The “big picture” has been given, let’s look at the implementation

Goal 1: Generate pages quickly

To do this, create two new folders, SRC and template, respectively. SRC is used to store the generated file directory, template directory is used to store the corresponding page template file: Ejs, table.service.js.ejs, report.vue.ejs, report.model.js.ejs (the relevant template code is in my Github repository). Then start writing the relevant code, as follows:

const chalk = require('chalk')
const fs = require('fs-extra')
const ejs = require('ejs')
const symbols = require('log-symbols')
const path = require('path')

// Information about supported templates
const map = {
  table: {
    'vue': {
      temp: 'table.vue.ejs'./ / template
      ext: 'vue'./ / the suffix
    },
    'js': {
      temp: 'table.service.js.ejs'.ext: 'service.js',}},report: {
    'vue': {
      temp: 'report.vue.ejs'.ext: 'vue',},'js': {
      temp: 'report.model.js.ejs'.ext: 'model.js',}}}/ * * *@params {*} The path path *@params {*} The options parameter * /
async function generate(url, options) {
  let template, tempName
  const { temp = 'table' } = options // Get the template to be generated. Since there is no support for template selection, give a default value first
  
  if (temp in map) {  // Check whether there is a corresponding template
    tempName = temp
    template = map[temp]
    try {
      for (const [key, value] of Object.entries(template)) {
        const {temp, ext} = value
        const baseUrl = 'src'
        const filepath = `${baseUrl}/${url}`
        const fileName = url.split('/').map(str= > str.slice(0.1).toUpperCase() + str.slice(1).toLowerCase()).join(' ') // Fill in the name information in the EJS template
        // Compile the template builder file
        await compile(filepath, temp, {
          name: fileName, ext, ... options, }) }console.log(symbols.success, chalk.cyan('πŸš€ generate success'))}catch (err) {
      console.log(symbols.error, chalk.red(`Generate failed. ${err}`))}}else { 
    console.log(symbols.error, chalk.red(`Sorry, don't support this template`)) // If not, prompt}}/** * Compile the template to generate the result *@params {*} Filepath specifies the generated filepath *@params {*} Templatepath templatepath *@params {*} The options to configure * /
const compile = async (filepath, templatepath, options) => {
  const cwd = process.cwd(); // The current working directory
  const targetDir = path.resolve(cwd,`${filepath}.${options.ext}`); // The generated file
  const tempDir = path.resolve(__dirname, `.. /template/${templatepath}`); // Template directory
  if (fs.existsSync(tempDir)) { // Check whether the template path exists
    const content = fs.readFileSync(tempDir).toString()
    const result = ejs.compile(content)(options)
    fs.writeFileSync(`${targetDir}`, result)
  } else {
    throw Error("Don't find target template in directory")}}module.exports = generate
Copy the code

The reason I’m going to keep my.vue files and.js files separate is because my project’s.vue files and.js files are in different directories, so it’s easier to keep them separate.

The same goes for registering directives in bin/index.js

// generate page
program
  .command('g <name>')
  .description('Generate template')
  .action((name, options) = > {
    require('.. /lib/generate')(name, options)
  })
Copy the code

Hello.service. js and hello.vue were successfully generated in the SRC folder. At the same time, the console gave a successful generation prompt.

The functionality is complete, but there are still many problems. For example, you can only enter a specific file name, not a path like hello/hello. In addition, if there is a file with the same name under the corresponding path, the scaffold does not give a hint and directly overwrites it, which certainly does not meet our requirements.

Therefore, next we need to optimize yyDS G instruction.

Support input path

We need to write a function to replace fs.writefilesync.

/ * * * *@param {*} Paths path *@param {*} Data Indicates the data to be written@param {*} The ext file suffix */
 function writeFileEnsure(paths, data, ext) {
  const cwd = process.cwd();
  const pathArr = paths.split('/')
  pathArr.reduce((prev, cur, index) = > {
    const baseUrl = path.resolve(cwd, `${prev}/${cur}`)
    if (index === pathArr.length - 1) {
      fs.writeFileSync(path.resolve(cwd, `${baseUrl}.${ext}`), data)
    } else {
      if(! fs.existsSync(baseUrl)) { fs.mkdirSync(path.resolve(cwd, baseUrl)) } }return prev + '/' + cur
  }, '. ')}Copy the code

Replace the old fs.writeFileSync with the newly written function compile:

// ...
// fs.writeFileSync(`${targetDir}`, result)
await writeFileEnsure(filepath, result, options.ext)
// ...
Copy the code

After rewriting, we try whether to support yyDS G hello/ Hello form, you can see the perfect implementation of the function πŸš€!

Keep up the good work, we continue to solve the file overwrite problem πŸ’ͺ. In order to better user experience, we need to use Inquirer to help us select the corresponding command in the command line through the interaction form of arrow keys and enter. That’s the second goal we need to accomplish.

Goal two: File overwrite interactive functionality

Start by introducing inquirer in generate.js

const inquirer = require('inquirer')
Copy the code

Then rewrite the compile function to determine if there are files with the same name in the current directory before writing them.

// ...
  if (fs.existsSync(tempDir)) { // Check whether the template path exists
    if (fs.existsSync(targetDir)) {
      const { action } = await inquirer.prompt([
        {
          name: 'action'.type: 'list'.message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`.choices: [{name: 'Overwrite'.value: 'overwrite' },
            { name: 'Cancel'.value: false}}]])if(! action) {throw Error('Cancel overite')}else if (action === 'overwrite') {
        console.log(` πŸ‘» o${chalk.cyan(targetDir)}`)
        await fs.remove(targetDir)
      }
    }
    // ...
  }
// ...
Copy the code

Now, try to continue with yyds G Hello on the console, and the following interface appears

chooseOverwriteIt will overwrite the original file, selectcancelThe prompt* Generate failed. Error: Cancel overwrite.

Of course, there are also grumpy friends who feel that it is troublesome to have to make choices every time. Because in some cases, users know exactly what they’re doing! Is there a way to override it without prompting? There are! And it’s easy. Just add a line of configuration when registering the instruction, and add another layer of judgment in the compile function with the parameters passed in from that configuration.

Goal 3: Overwrite files forcibly

Add the -f –force parameter

program
  .command('g <name>')
  .description('Generate template')
  .option('-f --force'.'Overwrite target directory if it exists')
  .action((name, options) = > {
    require('.. /lib/generate')(name, options)
  })
Copy the code

Modify the compile function

// ...
if (fs.existsSync(targetDir)) {
+  if(! options.force) {// Only if force is false will the logic of interactive prompts be entered
    const { action } = await inquirer.prompt([
      // ...
    ])
    // ...+}}// ...
Copy the code

Now just type yyds g hello -for yyds g hello –force to overwrite the same file.

At this point, we have accomplished three goals, but as you may have noticed, the map object of generate.js also supports the rapid generation of another template report page, but obviously our scaffolding does not support the generation of the corresponding template page. Don’t panic. Let’s take A sip of tea. β•­(Β°AΒ° ‘)

Ok, now let’s finish goal four together

Goal 4: Switch templates

We need to add an additional parameter to the registration so that we can enter the name of the template in the console.

// generate page
program
  .command('g <name>')
  .description('Generate template')
  .option('-t, --temp <name>'.'Auto generate <name> page. Currently support template [table, report]'.'table') // The last 'table' indicates that the default template table is used if no -t parameter is passed
  .option('-f --force'.'Overwrite target directory if it exists')
  .action((name, options) = > {
    require('.. /lib/generate')(name, options)
  })
Copy the code

In the generate.js function, there is already corresponding logic support. We can get the corresponding template name through options.

// ...
const { temp } = options // Obtain the template information in this line
if (temp in map) { 
    tempName = temp
    template = map[temp] 
/ /...
Copy the code

Finally, let’s look at the effect. Because the final effect is not convenient to display, so let’s look at the log information printed by yyds g report-t report.

It can be seen that temp corresponds to Report, so we will also get the EJS template corresponding to report for rendering. If we want to continue to add other templates, we can simply maintain the map object and the corresponding EJS template.

Goal 5: Parameter verification

Now we only have the final goal 5 to achieve, in fact, this is also very easy, just need to register the instruction place to add a judgment, here directly paste the code:

// Both packages need to be imported
const minimist = require('minimist')
const chalk = require('chalk') 
// ...
program
  .command('g <name>')
  .description('Generate template')
  .option('-t, --temp <name>'.'Auto generate <name> page. Currently support template [table, report]'.'table') // The last table indicates that the default template table is used if no -t parameter is passed
  .option('-f --force'.'Overwrite target directory if it exists')
  .action((name, options) = > {
    if (minimist(process.argv.slice(3))._.length > 1) {
      console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))}require('.. /lib/generate')(name, options)
  })
// ...
Copy the code

Now if the user does not enter a specified parameter, such as yyds g hello -t report XXXX, the warning Info will be given: You provided more than one argument. The first one will be used as the template’s name, the rest are ignored.

Configure and view information

Here is, in fact, this scaffold basic can already reached my request, but actually there is a small problem can be optimized, because I need to generate the path of the page is such SRC / / / / / / XXX XXX XXX XXX XXX XXX, so each generated pages, not every time take the a long list of nausea 🀒 path, thought of here, I was devastated.

But IT occurred to me that we could make the default path of the page into a configuration, so that in the console, we could just enter the final path, and then the path in the configuration item and the input path are spliced together, which is the final path we need to generate the page. And the default path can be supported in the console, through the form of instructions to change. Let’s look at the implementation.

First, create config.js under lib

const fse = require('fs-extra');
const path = require('path');
const symbols = require('log-symbols')
const chalk = require('chalk')
const config = {
  'tablevue': 'src/renderer/page/manage/desk'.'tablejs': 'src/renderer/page/manage/desk'.'reportvue': 'src/renderer/page/manage/report'.'reportjs': 'src/renderer/models/report',}const configPath = path.resolve(__dirname,'.. /config.json')

// Outputs a JSON file
async function defConfig() {
  try {
    await fse.outputJson(configPath, config)
  } catch (err) {
    console.error(err)
    process.exit()
  }
}

// Get the configuration information
async function getJson () {
  try {
    const config = await fse.readJson(configPath)
    console.log(chalk.cyan('current config:'), config)
  } catch (err) {
    console.log(chalk.red(err))
  }
}

/ / set the json
async function setUrl(name, link) {
  const exists = await fse.pathExists(configPath)
  if (exists){
    mirrorAction(name, link)
  }else{
    await defConfig()
    mirrorAction(name, link)
  }
}

async function mirrorAction(name, link){
  try {
    const config = await fse.readJson(configPath)
    config[name] = link
    await fse.writeJson(configPath, config)
    await getJson()
    console.log(symbols.success, 'πŸš€ Set the url successful.')}catch (err) {
    console.log(symbols.error, chalk.red('πŸ‘» Set the URL failed.${err}`))
    process.exit()
  }
}

module.exports = { setUrl, getJson, config}
Copy the code

Then rewrite the generate function. Instead of writing it as SRC, baseUrl reads the default path in the JSON file.

// ...
const { config } = require('./config')
const configPath = path.resolve(__dirname,'.. /config.json')
// ...
async function generate(url, options) {
  let template, tempName
  const { temp = 'table' } = options // Get the template to be generated
  
  if (temp in map) {  // Check whether there is a corresponding template
    tempName = temp
    template = map[temp]
+   let jsonConfig
+   await fs.readJson(configPath).then((data) = > {
+     jsonConfig = data
+   }).catch(() = > {
+     fs.writeJson(configPath, config)
+     jsonConfig = config
+   })
    try {
      for (const [key, value] of Object.entries(template)) {
        const {temp, ext} = value
        // const baseUrl = 'src'
+       const baseUrl = jsonConfig[`${tempName}${key}`]
        const filepath = `${baseUrl}/${url}`
        const fileName = url.split('/').map(str= > str.slice(0.1).toUpperCase() + str.slice(1).toLowerCase()).join(' ')
        // Compile the template builder file
        await compile(filepath, temp, {
          name: fileName, ext, ... options, }) }console.log(symbols.success, chalk.cyan('πŸš€ generate success'))}catch (err) {
      console.log(symbols.error, chalk.red(`Generate failed. ${err}`))}}else { 
    console.log(symbols.error, chalk.red(`Sorry, don't support this template`)) // If not, prompt}}// ...
Copy the code

Finally, unregister the instructions YYds m

program
    .command('m <template> <url>')
    .description("Set the template url.")
    .action((template, url) = > {
            require('.. /lib/config').setUrl(template, url)
    })

program
    .command('config')
    .description("see the current config.")
    .action((template, mirror) = > {
            require('.. /lib/config').getJson(template, mirror)
    })
Copy the code

After the above steps are done, let’s look at the results.

First, re-execute the yyDS G hello command to see if the path has changed. As you can see, the generated page will appear in the SRC/we configure the renderer/page/manage/desk directory, at the same time will generate a config. The json file, which stores the default path is our configuration.

Then we run yyds config to see if we can see the configuration information. You can see that the result is successfully printed as follows:

Then we try to modify the configuration by changing the directory of the vue file generated by the table template, yyds m tablevue SRC /desk

Finally to verify the result, re-execute yyds G Hello to see the generated path.

Perfect! Give yourself a big hand.

To optimize the

At this point the scaffolding function is almost complete. But we can still make some small optimizations.

Help information

program.on('--help'.() = > {
  console.log()
  console.log(`  Run ${chalk.cyan(`yyds <command> --help`)} for detailed usage of given command.`)
  console.log()
})

program.commands.forEach(c= > c.on('--help'.() = > console.log()))
Copy the code

Run HZZT –help for Detailed usage of given command Prompts the user for more detailed instructions.

A little ritual

With the figlet bag, we can do some fancy hints. After the generate.js function successfully generates the page, we show the big prompt yyds.

// ...
const { promisify } = require('util')
const figlet = promisify(require('figlet'))
// ...

// ...
console.log(symbols.success, chalk.cyan('πŸš€ generate success'))
const info = await figlet('yyds', {
  font: 'Ghost'.width: 100.whitespaceBreak: true
})
console.log(chalk.green(info), '\n')
// ...
Copy the code

Posted to NPM official website

At this point, the scaffolding function is complete. However, we still need one last step: Posting the scaffolding to the NPM website. The steps are as follows:

  1. new.npmignorefile
config.josn
node_modules/
src/
Copy the code

Exclude files to be published to the NPM repository

  1. Log in to the NPM account

If it is the first time to send packets, run the NPM adduser command and enter the user name, password, and email address to register packets. After completing this step, don’t forget to verify the account in the mailbox, otherwise the packet will send an error.

If the packets are not sent for the first time, run the NPM login command to enter the user name, password, email address, and login account.

  1. The local NPM image is lentnpm

If it is a taobao mirror, you need to switch to NPM. How to quickly view and switch, here is a recommended tool NRM.

NRM current // View the current mirror source NRM ls // Available mirror NRM use NPM // Switch mirrorsCopy the code
  1. release

Publish –access public publish –access public NPM publish –access public publish –access public

  1. unbundling

Run the NPM unlink command to unbind the downloaded NPM package. Otherwise, the downloaded NPM package cannot be verified because it is associated with the local YYDS directory.

If the unbinding fails, run NPM unlink -g yyds-cli. The unbinding fails, and the unbinding succeeds only when you run the preceding command. At first, NPM Unlink sent an error message telling me to bring packagename, but I still couldn’t unbind it. Later, I guess the reason is that the Node version is changed, so I tried to switch the Node version, although NPM unlink can be directly executed, but still failed to unbind. Finally, I run NPM unlink -g yyds -CLI with -g parameter. The reason did not check online, hope to know god informed.

  1. validation

After the package is successfully sent, global scaffolding is installed, and then go to the working directory to test the effect.

npm i -g yyds-cli && npm g hello
Copy the code

Seeing the page successfully generated under the project, I could not help but smile contemptuously (hum, also want to prevent me from getting off work at the point).

Say a point what

I wrote this tool partly out of laziness and partly out of interest, as if I were a front-end programmer by interest. Hope everybody looked at the officer looked to be able to leave valuable opinions, not stingy give advice (novice shiver).

Finally, the source of this article is also put on my small broken Github, like also hope to give a star.

Shoulders of giants:

  1. Front-end CLI scaffolding idea analysis – build from 0 to 1
  2. How is the Vue CLI implemented – terminal command line tools