Many little buddies have been struggling about what scaffolding is?In fact, the core function is to create the project initial file, the problem comes again, is there not enough scaffolding on the market, why do you want to write it yourself?
As soon as you mention scaffolding, you'll think vue-cli, create-react-app, dva-cli.... Their characteristics are, not to mention, exclusive!But in the development of the company, you will find the following series of problems!
- Business Types
- Multiple wheels, project upgrades, etc.
- Company code specification, not uniform
Before you develop your own cli, you must first see how some great CLIS are implemented!Although not the first to eat crabs, think about how you can eat better
1. Prerequisite modules
Let's start with the well-known vue-cli and see which npm packages he uses to implement it
- commander: parameter parsing--help actually helped him~
- inquirer: an interactive command-line tool that enables command-line selection
- download-git-repo: download templates from Git
- chalk: chalk helps us draw a variety of colors in the console
- Metasmith: Read all files for template rendering
- consolidate: unified template engine
Imagine what you want to do first:
Initialize project quick-cli create project-name from template
Initialization profile quick-cli config set repo-name
2. Project Creation
Say nothing more. We started creating projects and writing our own scaffolds~~
npm init -y # Initialize package.json npm install eslint husky --save-dev # eslint is responsible for code validation and husky provides git hook functionality npx eslint --init # Initialize eslint configuration file
2.1 Create folders
├── bin │ └── www // Root file for global command execution ├── package.json ├── src │ ├── main.js // Entry File │ └── utils // Storage Tool Method │── .huskyrc // git hook │── .eslintrc.json // Code Specification Checks
2.2 eslint configuration
Configure the code under the package.json check src folder
"scripts": { "lint":"eslint src" }
2.3 Configure husky
Check code compliance before git submission
{ "hooks": { "pre-commit": "npm run lint" } }
2.4 Link Global Package
Sets the WW file in the bin directory to be called when quick-cli is executed under the command
"bin": { "quick-cli": "./bin/www" }
main is used as the entry file in the www file and executed in the node environment
#! /usr/bin/env node require('../src/main.js');
Link packages to use globally
npm link
We have successfully used the quick-cli command on the command line and executed the main.js file!
3. Parse command line parameters
commander:The complete solution for node.js command-line interfaces
Blow a commander first. The commander can automatically generate help and parse option parameters!
Vue-cli--help like this!
Vue-cli create <project-namne>
3.1 Use commander
npm install commander
main.js is our entry file
const program = require('commander'); program.version('0.0.1') .parse(process.argv); // process.argv is the parameter that the user passes in from the command line
Execute quick-cli--help already has a hint!
This version number should be the version number of the current cli project, we need to get it dynamically, and for convenience we put all the constants in the constants folder under util
const { name, version } = require('../../package.json'); module.exports = { name, version, };
So we can get the version number dynamically
const program = require('commander'); const { version } = require('./utils/constants'); program.version(version) .parse(process.argv);
3.2 Configuration Instruction Commands
Traverse to generate the corresponding command, executing the action based on the functional configuration we want to achieve
const actionsMap = { create: { // Create Template description: 'create project', alias: 'cr', examples: [ 'quick-cli create <template-name>', ], }, config: { // Configuration Profile description: 'config info', alias: 'c', examples: [ 'quick-cli config get <k>', 'quick-cli config set <k> <v>', ], }, '*': { description: 'command not found', }, }; // Loop Creation Command Object.keys(actionsMap).forEach((action) => { program .command(action) // Name of the command .alias(actionsMap[action].alias) // Alias of command .description(actionsMap[action].description) // Description of the command .action(() => { // action console.log(action); }); }); program.version(version) .parse(process.argv);
3.3 Write help commands
Listen for help commands to print help information
program.on('--help', () => { console.log('Examples'); Object.keys(actionsMap).forEach((action) => { (actionsMap[action].examples || []).forEach((example) => { console.log(`${example}`); }); }); });
Now that we've configured the command line really well, let's get started!
4.create command
The main function of the create command is to pull the template from the git repository and download the corresponding version locally. If there are templates, they will be rendered according to the information the user fills in and generated to the directory where the command is currently running~
action(() => { // action if (action === '*') { // If the action does not match the description, the input is incorrect console.log(acitonMap[action].description); } else { // Pass in parameters by referencing the corresponding action file require(path.resolve(__dirname, action))(...process.argv.slice(3)); } }
Introduce files of corresponding modules dynamically according to different actions
Create create.js
// Create Project module.exports = async (projectName) => { console.log(projectName); };
Execute quick-cli create project to print out the project
4.1 Pull Items
We need to get all the template information in the warehouse. My templates are all on git. Here, for example, git, I use axios to get relevant information to ~~
npm i axios
Here with the help of github's api
const axios = require('axios'); // 1). Get a list of warehouses const fetchRepoList = async () => { // Get all the warehouse information in the current organization, where project templates are stored const { data } = await axios.get('https://api.github.com/orgs/quick-cli/repos'); return data; }; module.exports = async (projectName) => { let repos = await fetchRepoList(); repos = repos.map((item) => item.name); console.log(repos); };
I found that I had a very bad experience during installation without any hints, and the end result I hope is user-friendly!
4.2 inquirer & ora
Let's solve the problems mentioned above
npm i inquirer ora
module.exports = async (projectName) => { const spinner = ora('fetching repo list'); spinner.start(); // Start loading let repos = await fetchRepoList(); spinner.succeed(); // End loading // Select Template repos = repos.map((item) => item.name); const { repo } = await Inquirer.prompt({ name: 'repo', type: 'list', message: 'please choice repo template to create project', choices: repos, // Selection mode }); console.log(repo); };
The command line choices we see are basically based on inquirer s, which allow different ways of asking questions.
4.3 Getting Version Information
Like getting templates, we can do it again
const fetchTagList = async (repo) => { const { data } = await axios.get(`https://api.github.com/repos/quick-cli/${repo}/tags`); return data; }; // Get Version Information spinner = ora('fetching repo tags'); spinner.start(); let tags = await fetchTagList(repo); spinner.succeed(); // End loading // Select Version tags = tags.map((item) => item.name); const { tag } = await Inquirer.prompt({ name: 'tag', type: 'list', message: 'please choice repo template to create project', choices: tags, });
We find that every time we need to turn loading on and off, duplicate code can't be let go of it!Let's just wrap it up:
const wrapFetchAddLoding = (fn, message) => async (...args) => { const spinner = ora(message); spinner.start(); // Start loading const r = await fn(...args); spinner.succeed(); // End loading return r; }; // It's more comfortable to use this time. let repos = await wrapFetchAddLoding(fetchRepoList, 'fetching repo list')(); let tags = await wrapFetchAddLoding(fetchTagList, 'fetching tag list')(repo);
4.4 Download Project
We have successfully obtained the project template name and the corresponding version, so we can download it directly!
npm i download-git-repo
Unfortunately, this is not a promise method. It's OK to wrap it up by ourselves:
const { promisify } = require('util'); const downLoadGit = require('download-git-repo'); downLoadGit = promisify(downLoadGit);
node has provided you with a ready-made way to quickly convert an asynchronous api into a promise
Find a temporary directory to store the downloaded files before downloading. Let's continue configuring constants:
const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`;
Here we download the file to the.template file under the current user. Because the different directories of the system are obtained differently, process.platform gets win32 under windows. Here is mac, so the value obtained is darwin, and then the user directory is obtained according to the corresponding environment variables.
const download = async (repo, tag) => { let api = `quick-cli/${repo}`; // Download Project if (tag) { api += `#${tag}`; } const dest = `${downloadDirectory}/${repo}`; // Download the template to the appropriate directory await downLoadGit(api, dest); return dest; // Return to download directory }; // Download Project const target = await wrapFetchAddLoding(download, 'download template')(repo, tag);
For simple projects, you can copy the downloaded project directly to the directory where the command is currently executed.
Installing ncp allows you to copy files:
npm i ncp
Like this:
let ncp = require('ncp'); ncp = promisify(ncp); // Copy the downloaded file to the directory where the command is currently executed await ncp(target, path.join(path.resolve(), projectName));
Of course, there is more rigorous work to do here to determine if there are duplicate files in the current directory, etc. There are also many details to consider, such as whether to create a project multiple times using the downloaded templates, so that everyone can use them freely.
4.5 Template Compilation
Just now I said a simple file, of course a direct copy would be fine, but sometimes users can customize the contents of the download template. Take the package.json file for example, users can name items, set descriptions, etc. as prompted.
Here I added it to the project template ask.js
module.exports = [ { type: 'confirm', name: 'private', message: 'ths resgistery is private?', }, ... ]
Generate the final package.json based on the corresponding query
The ejs template is used in the downloaded template
{ "name": "vue-template", "version": "0.1.2", "private": "<%=private%>", "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build" }, "dependencies": { "vue": "^2.6.10" }, "autor":"<%=author%>", "description": "<%=description%>", "devDependencies": { "@vue/cli-service": "^3.11.0", "vue-template-compiler": "^2.6.10" }, "license": "<%=license%>" }
You should have thought of it when you write here!The core principle is to download the template file, iterate through the rendering templates according to the information the user fills in, and copy the rendered results to the directory where the command is executed
Install required modules
npm i metalsmith ejs consolidate
const MetalSmith = require('metalsmith'); // traverse folder let { render } = require('consolidate').ejs; render = promisify(render); // Packaging Rendering Method // No ask file description required compilation if (!fs.existsSync(path.join(target, 'ask.js'))) { await ncp(target, path.join(path.resolve(), projectName)); } else { await new Promise((resovle, reject) => { MetalSmith(__dirname) .source(target) // Traverse downloaded directories .destination(path.join(path.resolve(), projectName)) // Output rendered results .use(async (files, metal, done) => { // Boxes Ask Users const result = await Inquirer.prompt(require(path.join(target, 'ask.js'))); const data = metal.metadata(); Object.assign(data, result); // Put the results of the query in metadata to ensure they are available in the next Middleware delete files['ask.js']; done(); }) .use((files, metal, done) => { Reflect.ownKeys(files).forEach(async (file) => { let content = files[file].contents.toString(); // Get the contents of the file if (file.includes('.js') || file.includes('.json')) { // Templates are only possible if they are js or json if (content.includes('<%')) { // File uses <% before I need to compile content = await render(content, metal.metadata()); // Rendering templates with data files[file].contents = Buffer.from(content); // Rendered results can be replaced } } }); done(); }) .build((err) => { // Execution Middleware if (!err) { resovle(); } else { reject(); } }); }); }
The logic here is to implement template replacement as described above, and the functionality of this installation project is complete!We found that all the address paths used here were written to death, but we hope this is a more general scaffold that allows users to configure the pull address themselves to
5.config command
The main purpose of creating a new config.js is actually to read and write the configuration file. Of course, if the configuration file does not exist, you need to provide a default value. Let's write the constants first:
constants.js
Configurationconst configFile = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.quickrc`; // Storage location of configuration files const defaultConfig = { repo: 'quick-cli', // Default pulled repository name };
Write config.js
const fs = require('fs'); const { defaultConfig, configFile } = require('./util/constants'); module.exports = (action, k, v) => { if (action === 'get') { console.log('Obtain'); } else if (action === 'set') { console.log('Set up'); } // ... };
Configuration files of general rc types are in ini format, that is:
repo=quick-cli register=github
Download ini module parsing profile
npm i ini
The code here is simple, just file manipulation:
const fs = require('fs'); const { encode, decode } = require('ini'); const { defaultConfig, configFile } = require('./util/constants'); const fs = require('fs'); const { encode, decode } = require('ini'); const { defaultConfig, configFile } = require('./util/constants'); module.exports = (action, k, v) => { const flag = fs.existsSync(configFile); const obj = {}; if (flag) { // Profile Exists const content = fs.readFileSync(configFile, 'utf8'); const c = decode(content); // Parse file into object Object.assign(obj, c); } if (action === 'get') { console.log(obj[k] || defaultConfig[k]); } else if (action === 'set') { obj[k] = v; fs.writeFileSync(configFile, encode(obj)); // Write content to ini format into string console.log(`${k}=${v}`); } else if (action === 'getVal') { return obj[k]; } };
getVal This method is used to get the configuration variable when the create command is executed
const config = require('./config'); const repoUrl = config('getVal', 'repo');
So we can replace all quick-cli ps in the create method with the values we get!
The basic core of this approach is ok!The rest of you can expand on your own!
6. Project Publishing
Finally, to the final step, we pushed the project to npm, the process is no longer redundant!
nrm use npm npm publish # Successfully posted ~~
Installation can be done with NPM install quick-cli-g!
Hope you can get something out of this article!
If you think the article is good, please follow the WeChat Public Number and continue to get more content!