preface

Original address: github.com/Nealyang/Pe…

Scaffolding is actually familiar to most front ends, based on the two previous articles:

  • Exploration of front-end source architecture on auction detail page
  • Rax +Typescript+hooks project Architecture Thinking on a Single page

Basically, just to introduce the code organization of several of my project pages.

After using a few projects, I found that it was also quite handy, so I thought maybe I could get a CLI tool and unify the directory structure of the source code.

This will not only reduce a mechanical task but also unify the source code architecture. Student maintenance projects will also become less unfamiliar. It is true that there are some improvements. Although most of our pages are going bumblebee build 🥺…

function

In fact, cli tools are just some basic command running, CV method, no technical depth.

bin

The effect

bin

Project directory

Project directory

Code implementation

  • bin/index.js
#! /usr/bin/env node

'use strict';

const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('. '); const major = semver[0];  if (major < 10) {  console.error(  'You are running Node ' +  currentNodeVersion +  '.\n' +  'pmCli requires Node 10 or higher. \n' +  'Please update your version of Node.'  );  process.exit(1); }  require('.. /packages/initialization') ();Copy the code

Here is the entry file, relatively simple, is to configure an entry, incidentally verify the node version number

  • initialization.js

This file mainly configures some commands. In fact, it is relatively simple. You can check the configuration from commander and then configure it


Is according to their own needs to configure here is not redundant, in addition to the above, the following two points:

  • Function of entrance
 // Create project
  program
    .usage("[command]")
    .command("init")
    .option("-f,--force"."overwrite current directory")
 .description("initialize your project")  .action(initProject);   // Add a page  program  .command("add-page <page-name>")  .description("add new page")  .action(addPage);   // Add a module  program  .command("add-mod [mod-name]")  .description("add new mod")  .action(addMod);   // Add/modify.pmconfig. json  program  .command("modify-config")  .description("modify/add config file (.pmCli.config)")  .action(modifyCon);   program.parse(process.argv); Copy the code
  • out

The so-called backpocket is the input PM -cli without any command



pm-cli init

Now, before WE talk about init, there’s a technical background. Is our RAX project, based on def platform initialization, so it comes with a scaffold. But in source development, we change it a little bit. To avoid cognitive duplication, INIT has two functions:

  • init projectNameCreate one from zerodef init rax projectNameproject
  • In raxProject init will complement our unified source architecture based on the current architecture
process

init projectName

Here we demonstrate this in an empty directory

initProject
End of run diagram

init

init

The interaction of some of these issues is not covered, but some of the inquirer configuration issues. Not much reference value.

initProject
The entrance

The entry method is relatively simple. In fact, it is very simple to distinguish whether to run PM-CLI init based on the existing project initialization or to create a new RAx project. It is also simple to determine whether package.json is in the current directory


Although so judgment feeling is hasty point, but, you fine taste also really so! For the current directory that has package.json, I will also check that something else is not.

If package.json exists in the current directory, THEN I think you are a project in which you want to initialize the configuration of the auction source architecture. So I’m going to determine if the current project has already been initialized.

fs.existsSync(path.resolve(CURR_DIR, `. /${PM_CLI_CONFIG_FILE_NAME}`))
Copy the code

This is the content of the PM_CLI_CONFIG_FILE_NAME. Then give a hint. After all, you don’t need to repeat initialization. If you want to force another initialization, that’s fine!

pm-cli init -f
Copy the code

The preparatory work is in the early stages, and the final functionality to run is in the run method.

Verify name validity

There is also a function function that is very general, so I took it out in advance.

const dirList = fs.readdirSync(CURR_DIR);

checkNameValidate(projectName, dirList);
Copy the code
/ * ** Verify name validity* @param {string} name The name passed in modName/pageName* @param {Array}} validateNameList Array of invalid names* /
const checkNameValidate = (name, validateNameList = []) = > {  const validationResult = validatePageName(name);  if(! validationResult.validForNewPackages) { console.error(  chalk.red(  `Cannot create a mod or page named ${chalk.green(  `"${name}"`  )} because of npm naming restrictions:\n`  )  );  [ . (validationResult.errors || []),. (validationResult.warnings || []), ].forEach((error) = > {  console.error(chalk.red(` * ${error}`));  });  console.error(chalk.red("\nPlease choose a different project name."));  process.exit(1);  }  const dependencies = [  "rax". "rax-view". "rax-text". "rax-app". "rax-document". "rax-picture". ].sort();  validateNameList = validateNameList.concat(dependencies);   if (validateNameList.includes(name)) {  console.error(  chalk.red(  `Cannot create a project named ${chalk.green(  `"${name}"`  )} because a page with the same name exists.\n`  ) +  chalk.cyan(  validateNameList.map((depName) = > ` ${depName}`).join("\n")  ) +  chalk.red("\n\nPlease choose a different name.")  );  process.exit(1);  } }; Copy the code

In fact, it is to verify the validity of the name and exclude the same name. This utility function can be CV directly.

As shown in the diagram above, we have gone to the run method, and all that remains is some judgment.

  const packageObj = fs.readJSONSync(path.resolve(CURR_DIR, "./package.json"));
  // Judge is raX project
  if (
! packageObj.dependencies ||! packageObj.dependencies.rax ||! packageObj.name ) {  handleError("Must be initialized in raX 1.0 project");  }  // Determine the RAX version  let raxVersion = packageObj.dependencies.rax.match(/\d+/) | | []; if (raxVersion[0] != 1) {  handleError("Must be initialized in raX 1.0 project");  }   if(! isMpaApp(CURR_DIR)) { handleError('Does not support non${chalk.cyan('MPA')}The application uses pmCli ');  } Copy the code

Since these judgments are not very useful, I’ll skip them here and focus on the writing of some public methods.

addTsConfig

/ * ** Determine if the target project is TS and create a configuration file* /
function addTsconfig() {
  let distExist, srcExist;
 let disPath = path.resolve("./tsconfig.json");  let srcPath = path.resolve(__dirname, ".. /.. /ts.json");   try {  distExist = fs.existsSync(disPath);  } catch (error) {  handleError("Path resolution error code:0024, please contact @1凨");  }  if (distExist) return;  try {  srcExist = fs.existsSync(srcPath);  } catch (error) {  handleError("Path resolution error code:1233, please contact @1凨");  }  if (srcExist) {  // It exists locally  console.log(  chalk.red(Please use the encoding language${chalk.underline.red("Typescript")}`)  );  spinner.start("Creating configuration file for you: tsconfig.json");  fs.copy(srcPath, disPath)  .then((a)= > {  console.log();  spinner.succeed("Tsconfig. json configuration file has been created for you");  })  .catch((err) = > {  handleError("Tsconfig creation failed, please contact @1 凨");  });  } else {  handleError("Path resolution error code:2144, please contact @1凨");  } } Copy the code

The above code can be read by everyone, and the purpose of pasting this code is to make sure that when you write cli, you think more about boundary cases, existence judgments, and exceptions. Avoid unnecessary bugs

rewriteAppJson

/ * ** Rewrite app.json in the project* @param {string} distAppJson app.json path* /
function rewriteAppJson(distAppPath) {
 try {  let distAppJson = fs.readJSONSync(distAppPath);  if (  distAppJson.routes &&  Array.isArray(distAppJson.routes) &&  distAppJson.routes.length === 1  ) {  distAppJson.routes[0] = Object.assign({}, distAppJson.routes[0] and { title: Ali Auction. spmB: "B". spmA: "A code". });   fs.writeJSONSync(path.resolve(CURR_DIR, "./src/app.json"), distAppJson, {  spaces: 2. });  }  } catch (error) {  handleError(` rewrite${chalk.cyan("app.json")}Make a mistake,${error}`);  } } Copy the code

I don’t want to paste the other rewriting methods because it’s boring and repetitive. So let’s talk about public methods and their uses

Download the template

const templateProjectPath = path.resolve(__dirname, `.. /temps/project`);
// Download the template
await downloadTempFromRep(projectTempRepo, templateProjectPath);
Copy the code
/ * ** Download templates from remote repositories* @param {string} repo Remote warehouse address* @param {string} path Path* /
const downloadTempFromRep = async (repo, srcPath) => {  if (fs.pathExistsSync(srcPath)) fs.removeSync(`${srcPath}`);   await seriesAsync([`git clone ${repo} ${srcPath}`]).catch((err) = > {  if (err) handleError('Error downloading template: errorCode:${err}, please contact @1 凨 ');  });  if(fs.existsSync(path.resolve(srcPath,'./.git'))) { spinner.succeed(chalk.cyan('Remove.git from template directory'));  fs.remove(path.resolve(srcPath,'./.git'));  } }; Copy the code

Download template here I directly use shell script, because there are a lot of permissions involved here.

shell

// execute a single shell command where "cmd" is a string
exports.exec = function (cmd, cb) {
  // this would be way easier on a shell/bash script :P
  var child_process = require("child_process");
  var parts = cmd.split(/\s+/g);
 var p = child_process.spawn(parts[0], parts.slice(1), { stdio: "inherit" });  p.on("exit".function (code) {  var err = null;  if (code) {  err = new Error(  'command "' + cmd + '" exited with wrong status code "' + code + '"'  );  err.code = code;  err.cmd = cmd;  }  if (cb) cb(err);  }); };  // execute multiple commands in series // this could be replaced by any flow control lib exports.seriesAsync = (cmds) = > {  return new Promise((res, rej) = > {  var execNext = function () {  let cmd = cmds.shift();  console.log(chalk.blue("run command: ") + chalk.magenta(cmd));  shell.exec(cmd, function (err) {  if (err) {  rej(err);  } else {  if (cmds.length) execNext();  else res(null);  }  });  };  execNext();  }); }; Copy the code

copyFiles

/ * ** Copy page s* @param {array} filesArr* @param {function} errorCb Failed callback function* @param {success callback function} successCb success callback function* / const copyFiles = (filesArr, errorCb, successCb) = > {  try {  filesArr.map((filePathArr) = > {  if(filePathArr.length ! = =2) throw "Configuration file read/write error!";  fs.copySync(filePathArr[0], filePathArr[1]);  spinner.succeed(chalk.cyan(`${path.basename(filePathArr[1])}Initialization is complete));  });  } catch (error) {  console.log(error);   errorCb(error);  } }; Copy the code

After copying the remote code to the source directory temps/ and making a wave of changes, you still need to copy it to the project directory, so there is a method encapsulated here.

The configuration file

The configuration file is what I used to identify whether the current project is pmCli initialized. When addPage is added, some pages in the page use an external component, such as loadingPage

The configuration file

As above, initProject: true | false is used to identify the current warehouse.

[pageName] is used to indicate which pages are created using pmCli. Property type: ‘simpleSource’ | ‘withContext’ | ‘customStateManage’ is used to tell subsequent add – mod what add what kind of module.

At the same time, the content is encrypted because the configuration page is placed under the user’s project

The configuration file

encryption

const crypto = require('crypto');
function aesEncrypt(data) {
    const cipher = crypto.createCipher('aes192'.'PmCli');
    var crypted = cipher.update(data, 'utf8'.'hex');
    crypted += cipher.final('hex');
 return crypted; }  function aesDecrypt(encrypted) {  const decipher = crypto.createDecipher('aes192'.'PmCli');  var decrypted = decipher.update(encrypted, 'hex'.'utf8');  decrypted += decipher.final('utf8');  return decrypted; } module.exports = {  aesEncrypt,  aesDecrypt } Copy the code

Basically, that’s all for initializing the project, and the rest of the function is a rehash of these operations. Let’s take a quick look and make a point.

pm-cli add-page

addSimplePage
detail
Generated directory

The flow chart

The flow chart

The above functionality is similar to the code in initProject, except that some “business” cases are judged differently.

pm-cli add-mod

Custom status management module
Simple source code module
New modules

In fact, there is no special technical point in the addition of modules. First select the list of pages and then read the type of page in.pmcli.config. Add pages by type

function run(modName) {
  // A new module needs to be located
  modifiedCurrPathAndValidatePro(CURR_DIR);
  // Select the page where you can add modules
  pageList = Object.keys(pmCliConfigFileContent).filter((val) = > {
 returnval ! = ="initProject";  });  if (pageList.length === 0) {  handleError();  }   inquirer.prompt(getQuestions(pageList)).then((answer) = > {  const { pageName } = answer;  // modName is the same name  try {  checkNameValidate(  modName,  fs.readdirSync(  path.resolve(CURR_DIR, `./src/pages/${pageName}/components`)  )  );  } catch (error) {  console.log("Failed to read current page module list", error);  }   let modType = pmCliConfigFileContent[pageName].type;  inquirer.prompt(getInsureQuestions(modType)).then(async (ans) => {  if(! ans.insure) { modType = ans.type;  }  const distPath = path.resolve(  CURR_DIR,  `./src/pages/${pageName}/components`  );  const tempPath = path.resolve(__dirname, ".. /temps/mod");  // Download the template  await downloadTempFromRep(modTempRepo, tempPath);  try {  if (fs.existsSync(distPath)) {  console.log(chalk.cyanBright('Start module initialization'));  let copyFileArr = [  [  path.resolve(tempPath, `. /${modType}`),  path.resolve(distPath, `. /${modName}`), ]. ];  if(modType === 'customStateManage') { copyFileArr = [  [  path.resolve(tempPath,`. /${modType}/mod-com`),  path.resolve(distPath,`. /${modName}`) ]. [  path.resolve(tempPath,`. /${modType}/mod-com.d.ts`),  path.resolve(distPath,`.. /types/${modName}.d.ts`) ]. [  path.resolve(tempPath,`. /${modType}/mod-com.reducer.ts`),  path.resolve(distPath,`.. /reducers/${modName}.reducer.ts`) ]. ]  }  copyFiles(copyFileArr, (err) => {  handleError('Failed to copy configuration file', err);  });  if(! ans.insure) { console.log();  console.log(  chalk.underline.red(  'Please confirm page:${pageName}, in.pmcli. config  )  );  console.log();  }  modAddEndConsole(modName,modType);  } else {  handleError("There is a problem with the local file directory");  }  } catch (error) {  handleError("Error reading file directory, please contact @1凨");  }  });  }); } Copy the code

Correct CURR_DIR

When adding modules, I also did a personal touch. In case you think you need to go to pages to add mod, I support add-mod as long as you are in SRC, Pages, or the project root directory

/ * ** Correct the current path to the project path, mainly to prevent users from creating new modules on the current page* /
const modifiedCurrPathAndValidatePro = (proPath) = > {
  const configFilePath = path.resolve(CURR_DIR, `. /${PM_CLI_CONFIG_FILE_NAME}`);
 try {  if (fs.existsSync(configFilePath)) {  pmCliConfigFileContent = JSON.parse(  aesDecrypt(fs.readFileSync(configFilePath, "utf-8"))  );  if(! isTrue(pmCliConfigFileContent.initProject)) { handleError('Configuration file:${PM_CLI_CONFIG_FILE_NAME}Tampered with, please contact @1 凨 ');  }  } else if (  path.basename(CURR_DIR) === "pages" ||  path.basename(CURR_DIR) === "src"  ) {  CURR_DIR = path.resolve(CURR_DIR, ".. /");  modifiedCurrPathAndValidatePro(CURR_DIR);  } else {  handleError('The current project is not${chalk.cyan("pm-cli")}Initialize, cannot use this command ');  }  } catch (error) {  handleError("Failed to read project configuration file", error);  } }; Copy the code

pm-cli modify-config

Because before introduced the source of the page structure, I also applied to the project development. PmCli development, and added a new configuration file, local or encrypted. So isn’t my previous project need to add pages and can’t use this pmCli?

So, we added this feature:

modify-config:

  • Whether the current project existspmCliIf no, create a new one. If yes, modify it

Points to Note (Summary)

  • The CLI is a simple Node applet.fs-extra+ shellYou can play it. It’s very simple
  • Boundary cases and various human interactions need to be considered
  • Exception handling and exception feedback needs to be adequately addressed
  • Boring and repetitive work. Of course, you can use your imagination

THE LAST TIME


  • Thoroughly understand JavaScript execution mechanics
  • This: call, apply, bind
  • A thorough understanding of all JS prototype related knowledge points
  • Simple JavaScript modularity
  • How TypeScript can be advanced
  • Learn the paradigm of Redux from its source code

TODO

  • Integrated Release Scaffolding (React)
  • Transparent parameter transmission is supported
  • Vscode plug-in, panel operation

tool

There are a lot of tools available on the CLI. Here I mainly use some open source packages and methods that I copy from CRA.

commander

homePage:https://github.com/tj/commander.js

Node.js command line interface complete solution

Inquirer

homePage:https://github.com/SBoudrias/Inquirer.js

Components of an interactive command line user interface

fs-extra

homePage:https://github.com/jprichardson/node-fs-extra

Fs module comes with an external extension module of the file module

semver

homePage:https://github.com/npm/node-semver

Used for some operations on versions

chalk

homePage:https://github.com/chalk/chalk

Component that adds color to text on the command line

clui

Spinners, Sparklines, Progress bars design display components

homPage:https://github.com/nathanpeck/clui

download-git-repo

homePage:https://gitlab.com/flippidippi/download-git-repo

Node downloads and extracts a Git repository (GitHub, GitLab, Bitbucket)

ora

homePage:https://github.com/sindresorhus/ora

The command line loading effect is similar to the previous one

shelljs

homePage:https://github.com/shelljs/shelljs

Node runs components of the shell across ends

validate-npm-package-name

homePage:https://github.com/npm/validate-npm-package-name

Check the validity of the package name

blessed-contrib

homePage:https://github.com/yaronn/blessed-contrib

Command line visual components

These tools were intended to be a separate article, but the list of articles is not very useful. It’s easy to forget mainly, so I’ve covered it here. Function and effect, we check and test by ourselves. Some of the better methods in CRA are listed at the end of this article. For CRA source code, check out my previous post: Github /Nealyang

Nice method/package in CRA

  • commander: To summarize,NodeCommand interface, that is, you can use it to administerNodeCommand.NPM address
  • envinfo: Displays information about the current operating system environment and the specified package.NPM address
  • fs-extra: External dependencies,NodeExternal extension module for the built-in file moduleNPM address
  • semver: External dependencies for comparisonNodeversionNPM address
  • checkAppName(): used to check whether the file name is valid.
  • isSafeToCreateProjectIn(): Checks whether the folder is secure
  • shouldUseYarn(): Used for testingyarnWhether it has been installed on the machine
  • checkThatNpmCanReadCwd(): Used for testingnpmWhether to execute in the correct directory
  • checkNpmVersion(): Used for testingnpmWhether it has been installed on the machine
  • validate-npm-package-name: External dependencies, check whether the package name is valid.NPM address
  • printValidationResults(): function reference, this function is what I call a very simple type, inside the received error message to print a loop, nothing to say.
  • execSync: since the referencechild_process.execSyncIs used to execute the child process that needs to be executed
  • cross-spawn:NodeCross-platform solutions, solutions inwindowsAll kinds of questions. Used to performnodeProcess.NPM address
  • dns: checks whether a request can be made to the specified address.NPM address

reference

  • xBuild

  • The depth resolutioncreate-react-appThe source code

  • Create-react-app source code react-scripts

  • 50 of the best command line tools to use

Technical communication

Full stack front-end AC group