preface

2022 is more than a quarter of the way through, and I don’t seem to have kept my promise of one article a month. Recently, the company is doing some front-end engineering related things, although the preparation of component library was blocked by the leader, but before this, I wrote a scaffold tool, after all, scaffolding tools are rampant in this environment, so of course I also want to write some.

  • preface
  • The final result
  • Support functions
  • The development of
  • Initialize the project
    • Set project entry
    • Other bells and whistles
  • Project template
  • release
  • conclusion
  • reference
  • conclusion

The final result

Support functions

  • Choose web terminal or mobile terminal;
  • Choose react or VUE project framework independently;
  • Independently choose whether to set remote Git address;
  • Support for custom variable substitution in project templates;
  • You can choose NPM, CNPM, and YARN.
  • Supports online update using the update command.
  • Select whether to overwrite existing files and directories.

The development of

Initialize the project

Json file in the package.json folder and set the common fields. The Settings are as follows:

{
  "name": "new-cli"."version": "1.0.0"."description": "a react project cli, help you create a react project quickly"."bin": {
    "new-cli": "bin/www.js"
  },
  "dependencies": {
    "boxen": "^ 5.1.2." "."chalk": "^ 4.1.2." "."commander": "^ 9.1.0"."consolidate": "^ 0.16.0"."cross-spawn": "^ 7.0.3." "."download-git-repo": "^ 3.0.2." "."ejs": "^ 3.1.6." "."fs-extra": "^ 10.0.1." "."inquirer": "^ 8.2.1." "."metalsmith": "^" 2.4.2."ora": "^ 5.4.1." "."figlet": "^ 1.5.2." "."semver": "^ 7.3.5." "."shelljs": "^ 0.8.5"
  },
  "repository": {
    "type": "git"."url": "https://github.com/BoWang816/new-cli.git"
  },
  "keywords": [
    "cli"."react"]."author": "But the morning"."publishConfig": {
    "registry": "Private warehouse address"
  },
  "engines": {
    "node":"^ 12.20.0 | | > = 14"}}Copy the code

NPM install -g new-cli after the above setup, the scaffold will be named new-cli. The name below bin is used to set the command to be executed by the scaffold, and is used as an entry file from the bin/www.js file. In dependencies, we need to install the lower version of the package because boxen, Chalk, figlet and other dependencies do not support requier anymore. PublishConfig can set the NPM address that needs to be published at that time. If you set up the NPM private server, you can publish to your private server by setting Registry.

Set project entry

After package.json is set up, we can create an entry file, which is www.js under bin. In fact, you can put your entry file in the root directory as much as you like. Of course, if you put it in the root directory, you can change it to new-cli: ‘./www.js’ under bin. www.js mainly introduces tools such as Commander and Inquirer to initialize scaffolding tools. Because www.js is going to run as a Node script, you need to declare the environment at the top: #! /usr/bin/env node: init, update, and help are included in this scaffold, and help is supported by Commander itself, just a bit of customization.

  • Initialize init command, update command, help command

    Const {program} = require(“commander”); const {program} = require(“commander”); The scaffolding tool is the body of the tool, and we initialize the associated commands:

      #! /usr/bin/env node
      / / into the commander
      const {program} = require("commander");
      
      // initialize init command, project-name is your project name and project folder name
      program.command("init <project-name>")
              // init command description
             .description("create a new project name is <project-name>")
             // init command argument, because the support for overwriting folders will be set later, so a -f argument is provided
             .option("-f, --force"."overwrite target directory if it exists")
             // init names things to do after execution
             .action(() = > { 
                  console.log('doSomething');
              });
             
      program.command("update")
              .description("update the cli to latest version")
              // Update automatically detects updates done after the command is executed
              .action(async() = > {// await checkUpdate();
                  console.log('update');
              });
      
      program.on("--help".() = > {
          // Listen for the --help command to print a prompt
          console.log(figlet.textSync("new-cli", {
              font: "Standard".horizontalLayout: 'full'.verticalLayout: 'fitted'.width: 120.whitespaceBreak: true
          }));
      });   
      
      // This must not be forgotten, and must be the last!!
      program.parse(process.argv);      
    Copy the code

With this setup, we can actually use the basic commands. Local debugging can be done in two waysnpm linkThe command links the scaffolding tool we wrote directly to the local global NPM, or directly throughnode bin/www.jsExecute the js file directly; we’ll use the latter here.

  • Extend init command

    Next we need to extend the init naming, which means doing something in the action. First of all, we provide the parameters of the -f option, the purpose is to at the time of initialization program detected a folder with the same are covered, so in the first step in the initialization program we need to detect whether there is a folder with the same under the current path, and in the absence of Settings – f giving prompt information, at the same time after setting the -f given secondary prompt, Agree to override and start initializing the project. So for the action function to do the following, we need to introduce chalk, Paht, Fs-Extray, and then create.

const chalk = require("chalk");
const path = require("path");
const fs = require('fs-extra');
const figlet = require('figlet');
const create = require('.. /utils/create');
    
program
    .command("init <project-name>")
    .description("create a new project name is <project-name>")
    .option("-f, --force"."overwrite target directory if it exists")
    .action(async (projectName, options) => {
        const cwd = process.cwd();
        // Concatenate to the destination folder
        const targetDirectory = path.join(cwd, projectName);
        // If the destination folder already exists
        if (fs.existsSync(targetDirectory)) {
            if(! options.force) {// If no -f is set, prompt and exit
                console.error(chalk.red(`Project already exist! Please change your project name or use ${chalk.greenBright(`new-cli create ${projectName} -f`)} to create`))
                return;
            }
            // If -f is set, ask whether to overwrite the original folder again
            const {isOverWrite} = await inquirer.prompt([{
                name: "isOverWrite".type: "confirm".message: "Target directory already exists, Would you like to overwrite it?".choices: [{name: "Yes".value: true},
                    {name: "No".value: false}}]]);// If you want to overwrite, start deleting the original folder
            if (isOverWrite) {
                const spinner = ora(chalk.blackBright('The project is Deleting, wait a moment... '));
                spinner.start();
                await fs.removeSync(targetDirectory);
                spinner.succeed();
                console.info(chalk.green("✨ Deleted Successfully, start init project..."));
                console.log();
                // After successful deletion, start initializing the project
                // await create(projectName);
                console.log('init project overwrite');
                return;
            }
            console.error(chalk.green("You cancel to create project"));
            return;
        }
        // Initializes the project directly if there is no folder with the same name in the current path
        // await create(projectName);
        console.log('init project');
    });
Copy the code

Let’s see what it looks like now:

  • Create the create method

    In the previous step, after overwriting the file with the same name, we started initializing the project with the await Create (projectName) method, and then we started developing the Create method. Create a new folder in the root directory called utils (lib or ✨) and create a new file under utils called create.js. In this file, we will set up the execution of some questions in the download initialization project. The main contents are as follows:

      const inquirer = require("inquirer");
      const chalk = require("chalk");
      const path = require("path");
      const fs = require("fs");
      const boxen = require("boxen");
      const renderTemplate = require("./renderTemplate");
      const downloadTemplate = require('./download');
      const install = require('./install');
      const setRegistry = require('./setRegistry');
      const {baseUrl, promptList} = require('./constants');  
       
      const go = (downloadPath, projectRoot) = > {
          return downloadTemplate(downloadPath, projectRoot).then(target= > {
          // Download the template
              return {
                  downloadTemp: target
              }
          })
      }
      module.exports = async function create(projectName) {
          // Verify the validity of the project name. The project name only supports strings and numbers, because the name will be used in package.json and many other places in the project, so no special characters can exist
          const pattern = /^[a-zA-Z0-9]*$/;
          if(! pattern.test(projectName.trim())) {console.log(`\n${chalk.redBright('You need to provide a projectName, and projectName type must be string or number! \n')}`);
              return;
          }
          / / to ask
          inquirer.prompt(promptList).then(async answers => {
              // Target folder
              const destDir = path.join(process.cwd(), projectName);
              // Download the address
              const downloadPath = `direct:${baseUrl}/${answers.type}-${answers.frame}-template.git#master`
              // Create a folder
              fs.mkdir(destDir, {recursive: true}, (err) = > {
                  if (err) throw err;
              });
      
              console.log(`\nYou select project template url is ${downloadPath} \n`);
              // Start downloading
              const data = await go(downloadPath, destDir);
              // Start rendering
              await renderTemplate(data.downloadTemp, projectName);
              // Whether to automatically install dependencies. By default, no
              const {isInstall, installTool} = await inquirer.prompt([
                  {
                      name: "isInstall".type: "confirm".default: "No".message: "Would you like to help you install dependencies?".choices: [{name: "Yes".value: true},
                          {name: "No".value: false}},// Which package management tool to use when you select install dependencies
                  {
                      name: "installTool".type: "list".default: "npm".message: 'Which package manager you want to use for the project? '.choices: ["npm"."cnpm"."yarn"].when: function (answers) {
                          returnanswers.isInstall; }}]);// Start installing dependencies
              if (isInstall) {
                  await install({projectName, installTool});
              }
      
              // Whether the warehouse address is set
              if (answers.setRegistry) {
                  setRegistry(projectName, answers.gitRemote);
              }
    
              // Project downloaded successfully
              downloadSuccessfully(projectName);
          });
      }
    Copy the code
    • increate.jsIn the file, we first determine whether the initialized project name contains special characters. If it does, we will give an error message and terminate the project initialization. If the project name is valid, start asking the user for the project template they want:

    We are extracting the list of these queries as constants, and we are extracting the address of the template as constants, so we need to create one under the utils folderconstants.js, which contains the following contents:

      /**
       * constants.js
       * @author kechen
       * @since 2022/3/25 * /
    
      const { version } = require('.. /package.json');
    
      const baseUrl = 'https://github.com/BoWangBlog';
      const promptList = [
          {
              name: 'type'.message: 'Which build tool to use for the project? '.type: 'list'.default: 'webpack'.choices: ['webpack'.'vite'],}, {name: 'frame'.message: 'Which framework to use for the project? '.type: 'list'.default: 'react'.choices: ['react'.'vue'],}, {name: 'setRegistry'.message: "Would you like to help you set registry remote?".type: 'confirm'.default: false.choices: [{name: "Yes".value: true},
                  {name: "No".value: false}]}, {name: 'gitRemote'.message: 'Input git registry for the project: '.type: 'input'.when: (answers) = > {
                  return answers.setRegistry;
              },
              validate: function (input) {
                  const done = this.async();
                  setTimeout(function () {
                      // Check whether it is empty or a string
                      if(! input.trim()) { done('You should provide a git remote url');
                          return;
                      }
                      const pattern = /^(http(s)? : \ \ / (/ [^ \] +? \/){2}|git@[^:]+:[^\/]+? / /). *? .git$/;
                      if(! pattern.test(input.trim())) { done('The git remote url is validate',);return;
                      }
                      done(null.true);
                  }, 500); }}];module.exports = {
          version,
          baseUrl,
          promptList
      }
    Copy the code

Where version is the scaffolding version number, baseUrl is the basic address for downloading the project template, and promptList is the list of questions asked to users. The specific promptList is written according to the inquirer. Prompt () method. I will attach the official document address for the details. You can play by yourself.

  • After receiving feedback from the user through inquirer.prompt(), we will get the relevant field values, and then we will concatenate the address of the downloaded project template, and start downloading the project template. Here we write the go function and the renderTemplate function, one to download the project template and the other to render the project template (because variable substitution is involved). The go function actually uses a downloadTemplate method imported from the outside, so we need to focus on the downloadTemplate and renderTemplate methods, which is the focus of the next section.

  • Create the Download method

    In the utils folder, create a new file named download.js with the following contents:

    /** * download * download.js *@author kechen
     * @since 2022/3/25 * /
    
    const download = require('download-git-repo')
    const path = require("path")
    const ora = require('ora')
    const chalk = require("chalk");
    const fs = require("fs-extra");
    
    module.exports = function (downloadPath, target) {
        target = path.join(target);
        return new Promise(function (resolve, reject) {
            const spinner = ora(chalk.greenBright('Downloading template, wait a moment... \r\n'));
            spinner.start();
    
            download(downloadPath, target, {clone: true}, async function (err) {
                if (err) {
                    spinner.fail();
                    reject(err);
                    console.error(chalk.red(`${err}download template failed, please check your network connection and try again`));
                    await fs.removeSync(target);
                    process.exit(1);
                } else {
                    spinner.succeed(chalk.greenBright('✨ Download template successfully, start to config it: \n')); resolve(target); }})})}Copy the code

In this file, we use the download-git-repo third-party library to download the project template. Because the download result of download-git-repo is success or failure, if we use it directly in asynchronous mode, there will be problems. Therefore, it is encapsulated as a promise. When err is executed, an exception will be thrown to the user. If successful, the target folder path will be returned for subsequent use. In create.js, we use the go function. After the go function is successfully executed, a data will be returned, in which the path of the project to be downloaded to the specific folder is obtained. In fact, the resolve result of the promise in Download is obtained. Now that the project template has been downloaded into this folder, renderTemplate is ready to start.

  • Create the renderTemplate method

In utils directory, create a new file called renderTemplate, js, the main purpose of this function is to initialize the project set variables to replace, mainly use the metalSmith and consolidate the two third-party packages, by traversing the initialization files in the project, Convert it to an EJS template and replace the associated variables. This method refers to vWW-CLI. It reads the ask.ts file in the project template to obtain the customized query list in the project template, and then performs the file template engine rendering to replace the set variables. The main contents are as follows:

   /** * rendertemplate.js *@author kechen
    * @since 2022/3/24 * /
   const MetalSmith = require('metalsmith'); 
   const {render} = require('consolidate').ejs;
   const {promisify} = require('util');
   const path = require("path");
   const inquirer = require('inquirer');
   const renderPro = promisify(render);
   const fs = require('fs-extra');
   
   module.exports = async function renderTemplate(result, projectName) {
       if(! result) {return Promise.reject(new Error('Invalid directory:${result}`))}await new Promise((resolve, reject) = > {
           MetalSmith(__dirname)
               .clean(false)
               .source(result)
               .destination(path.resolve(projectName))
               .use(async (files, metal, done) => {
                   const a = require(path.join(result, 'ask.ts'));
                   // Read the list of queries set in the ask.ts file
                   let r = await inquirer.prompt(a);
                   Object.keys(r).forEach(key= > {
                       // Clear the Spaces before and after the input, otherwise an error will be reported when package.json is read when installing dependenciesr[key] = r[key]? .trim() ||' ';
                   })
                   const m = metal.metadata();
                   consttmp = { ... r,// Convert all used names to lowercase letters
                       name: projectName.trim().toLocaleLowerCase()
                   }
                   Object.assign(m, tmp);
                   // Delete the files in the template when done
                   if (files['ask.ts']) {
                       delete files['ask.ts'];
                       await fs.removeSync(result);
                   }
                   done()
               })
               .use((files, metal, done) = > {
                   const meta = metal.metadata();
                   // Set of file name extensions to be replaced
                   const fileTypeList = ['.ts'.'.json'.'.conf'.'.xml'.'Dockerfile'.'.json'];
                   Object.keys(files).forEach(async (file) => {
                       let c = files[file].contents.toString();
                       // Find the variables set in the project template and replace them
                       for (const type of fileTypeList) {
                           if (file.includes(type) && c.includes('< %')) {
                               c = awaitrenderPro(c, meta); files[file].contents = Buffer.from(c); }}}); done() }) .build((err) = >{ err ? reject(err) : resolve({resolve, projectName}); })}); };Copy the code

With the renderTemplate method, we have almost completed the main function of our scaffold. We can implement the init command to create the project. RemoveSync (result);}}}}}}}}}}}}} This file can not be deleted, but plus logically unreasonable, specific reasons have not been found, know friends can leave a message to explain, very grateful. Now that we’re done with the initialization of the project, we’re ready for some extensions.

  • Create the setRegistry method

    In the utils folder, create a new file called setregistry. js to help users initialize the git address of their project. During user creation, select whether to automatically set the git repository address.

      /** * set the repository address * setregistry.js *@author kechen
       * @since 2022/3/28 * /
      
      const shell = require("shelljs");
      const chalk = require("chalk");
      
      module.exports = function setRegistry(projectName, gitRemote) {
          shell.cd(projectName);
          if (shell.exec('git init').code === 0) {
              if (shell.exec(`git remote add origin ${gitRemote}`).code === 0) {
                  console.log(chalk.green('✨ \n Set registry Successfully, now your local gitRemote is${gitRemote} \n`));
                  return;
              }
              console.log(chalk.red('Failed to set.'));
              shell.exit(1); }};Copy the code
  • Create the install method

    In the utils folder, create a new file called install.js to help users automatically install dependencies. The main content is as follows:

     /** * Install dependency * install.js *@author kechen
       * @since 2022/3/22 * /
      const spawn = require("cross-spawn");
      
      module.exports = function install(options) {
          const cwd = options.projectName || process.cwd();
          return new Promise((resolve, reject) = > {
              const command = options.installTool;
              const args = ["install"."--save"."--save-exact"."--loglevel"."error"];
              const child = spawn(command, args, {cwd, stdio: ["pipe", process.stdout, process.stderr]});
      
              child.once("close".code= > {
                  if(code ! = =0) {
                      reject({
                          command: `${command} ${args.join("")}`
                      });
                      return;
                  }
                  resolve();
              });
              child.once("error", reject);
          });
      };
    Copy the code
  • Create the checkUpdate method

    In the utils folder, create a new file called checkupdate. js to help users automatically detect and update scaffolding.

      /** * check for updates * checkupdate.js *@author kechen
       * @since 2022/3/23 * /
      const pkg = require('.. /package.json');
      const shell = require('shelljs');
      const semver = require('semver');
      const chalk = require('chalk');
      const inquirer = require("inquirer");
      const ora = require("ora");
      
      const updateNewVersion = (remoteVersionStr) = > {
          const spinner = ora(chalk.blackBright('The cli is updating, wait a moment... '));
          spinner.start();
          const shellScript = shell.exec("npm -g install new-cli");
          if(! shellScript.code) { spinner.succeed(chalk.green(`Update Successfully, now your local version is latestVersion: ${remoteVersionStr}`));
              return;
          }
          spinner.stop();
          console.log(chalk.red('\n\r Failed to install the cli latest version, Please check your network or vpn'));
      };
      
      module.exports = async function checkUpdate() {
          const localVersion = pkg.version;
          const pkgName = pkg.name;
          const remoteVersionStr = shell.exec(
              `npm info ${pkgName}@latest version`,
              {
                  silent: true,
              }
          ).stdout;
      
          if(! remoteVersionStr) {console.log(chalk.red('Failed to get the cli version, Please check your network'));
              process.exit(1);
          }
          const remoteVersion = semver.clean(remoteVersionStr, null);
      
          if(remoteVersion ! == localVersion) {// Checks if the locally installed version is the latest version, and if not, asks if it is automatically updated
              console.log(`Latest version is ${chalk.greenBright(remoteVersion)}, Local version is ${chalk.blackBright(localVersion)} \n\r`)
      
              const {isUpdate} = await inquirer.prompt([
                  {
                      name: "isUpdate".type: "confirm".message: "Would you like to update it?".choices: [{name: "Yes".value: true},
                          {name: "No".value: false}}]]);if (isUpdate) {
                  updateNewVersion(remoteVersionStr);
              } else {
                  console.log();
                  console.log(`Ok, you can run ${chalk.greenBright('wb-cli update')} command to update latest version in the feature`);
              }
              return;
          }
          console.info(chalk.green("Great! Your local version is latest!"));
      };
    Copy the code

    It is important to note that because scaffolding is installed globally, permissions are involved, so you need to use sudo new-cli update on MAC, while you need to open the command-line tool as an administrator and execute new-CLI update on Windows. At this point, our scaffolding is almost complete.

Other bells and whistles

Create. Js has a downloadSuccessfully created method at the end of it. The main content is as follows:

const downloadSuccessfully = (projectName) = > {
    const END_MSG = `${chalk.blue("🎉 created project" + chalk.greenBright(projectName) + " Successfully")}\n\n 🙏 Thanks for using wb-cli !`;
    const BOXEN_CONFIG = {
        padding: 1.margin: {top: 1.bottom: 1},
        borderColor: 'cyan'.align: 'center'.borderStyle: 'double'.title: '🚀 Congratulations'.titleAlignment: 'center'
    }

    const showEndMessage = () = > process.stdout.write(boxen(END_MSG, BOXEN_CONFIG))
    showEndMessage();

    console.log('👉 Get started with the following commands:');
    console.log(`\n\r\r cd ${chalk.cyan(projectName)}`);
    console.log("\r\r npm install");
    console.log("\r\r npm run start \r\n");
}
Copy the code

This is what it looks like, and here I’m just taking a screenshot of what I did before.

Project template

We need to create a project template that contains an ask.ts file in the root directory, and the rest is just like normal projects. The file content of aks.

/**
 * demo
 * aks.ts
 * @author kechen
 * @since 2022/3/24 * /

module.exports = [
  {
    name: 'description'.message: 'Please enter project description:'}, {name: 'author'.message: 'Please enter project author:'}, {name: 'apiPrefix'.message: 'Please enter project apiPrefix:'.default: 'the API / 1.0'.// @ts-ignore
    validate: function (input) {
      const done = this.async();
      setTimeout(function () {
        // Check whether it is empty or a string
        if(! input.trim()) { done('You can provide an apiPrefix, or not it will be default [API /1.0] ',);return;
        }
        const pattern = /[a-zA-Z0-9]$/;
        if(! pattern.test(input.trim())) { done('The apiPrefix is must end with letter or number, like default [API /1.0] ',);return;
        }
        done(null.true);
      }, 300); }, {},name: 'proxy'.message: 'Please enter project proxy:'.default: 'https://www.test.com'.// @ts-ignore
    validate: function (input) {
      const done = this.async();
      setTimeout(function () {
        // Check whether it is empty or a string
        if(! input.trim()) { done('You can provide a proxy, or not it will be default [https://www.test.com] ',);return;
        }
        const pattern =
          /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])? /;
        if(! pattern.test(input.trim())) { done('The proxy is must end with letter or number, like default [https://www.test.com] ',);return;
        }
        done(null.true);
      }, 300); }},];Copy the code

Here, I set four variables, namely description, author, apiPrefix and proxy, which can be used in the way of <%= var %>. Var can be any variable you set in ask.ts. ** The file type to be replaced must be the file with the suffix set in the renderTemplate function mentioned above. ** In this way, you can add variables freely to the project template without updating the scaffolding tool.

{
  "name": "xasrd-fe-mobile"."description": "<%= description %>"."private": true."author": "<%= author %>"
}

Copy the code

Now that our scaffolding is all developed, it’s time to publish to NPM or NPM private server.

release

As mentioned above, if you need to publish the NPM private server, you need to configure the publishConfig in package.json and point to the address of the NPM private server. To publish, you need to use the following command:

  • Private NPM release

    • Log in private servers

    NPM login –registry=http://xxxxx XXXXX is your private server address

    • release

    npm publish

  • Official NPM release

    • directlynpm loginAgain,npm publish
    • The premise is that your NPM source points to the official NPM
  • NPM publishing is automatically triggered by github Action

    • For details, see: Developing an NPM package that encapsulates common front-end utility functions

It is important to note that the version number in package.json cannot be the same when you publish it.

conclusion

At this point, we have fully developed a relatively simple front-end scaffolding tool and are ready to publish. There are plenty of third-party toolkits available, but since the interaction is relatively simple, you can also get creative and do some more fancy extensions. The sample demo will not put, basically all the content is mentioned above, you can play freely. Of course, based on this set I also wrote an address is www.npmjs.com/package/wb-… However, due to lack of time recently, the project template is not available, and it cannot be run completely for the time being. It will be gradually updated later.

reference

  • inquirer
  • boxen
  • vee-cli
  • Document the development of an NPM package that encapsulates common front-end utility functions

conclusion

Finally, I hope that after reading this article, it will be helpful to you. If you are diligent, you can write it by hand. In addition, I hope you can pay attention to my Github, ha ha ha, show you snake!

The GridManager plugin supports React and Vue.

Next time, I will bring you some introduction of my commonly used Mac software, which can help you greatly improve your work efficiency in daily development and work!! You can get a preview of The Mac software recommendations.

Thanks for reading!!