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 🥺…
In fact, cli tools are just some basic command running, CV method, no technical depth.
bin
The effect
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 projectName
Create one from zerodef init rax projectName
project- In raxProject init will complement our unified source architecture based on the current architecture
init projectName
Here we demonstrate this in an empty directory
init
The interaction of some of these issues is not covered, but some of the inquirer configuration issues. Not much reference value.
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
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
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
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
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 exists
pmCli
If no, create a new one. If yes, modify it
Points to Note (Summary)
- The CLI is a simple Node applet.
fs-extra
+shell
You 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,Node
Command interface, that is, you can use it to administerNode
Command.NPM addressenvinfo
: Displays information about the current operating system environment and the specified package.NPM addressfs-extra
: External dependencies,Node
External extension module for the built-in file moduleNPM addresssemver
: External dependencies for comparisonNode
versionNPM addresscheckAppName()
: used to check whether the file name is valid.isSafeToCreateProjectIn()
: Checks whether the folder is secureshouldUseYarn()
: Used for testingyarn
Whether it has been installed on the machinecheckThatNpmCanReadCwd()
: Used for testingnpm
Whether to execute in the correct directorycheckNpmVersion()
: Used for testingnpm
Whether it has been installed on the machinevalidate-npm-package-name
: External dependencies, check whether the package name is valid.NPM addressprintValidationResults()
: 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.execSync
Is used to execute the child process that needs to be executedcross-spawn
:Node
Cross-platform solutions, solutions inwindows
All kinds of questions. Used to performnode
Process.NPM addressdns
: checks whether a request can be made to the specified address.NPM address
reference
-
xBuild
-
The depth resolution
create-react-app
The 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 | ||