Advanced series of large front end -- realize your own scaffold from zero

preface

With the increasing complexity of front-end development, a large number of js frameworks have emerged, and almost every one is equipped with supporting construction tools, such as Vue cli, create react AP, @ angular/cli, @ nestjs/cli and so on. These scaffolds can quickly help developers initialize configuration, build directory structure and build projects. Although these scaffolds are quite excellent and can almost meet most development needs, they may need to be adjusted according to business needs in a specific development scenario, which requires a certain understanding of the internal operation mechanism of scaffolds.

The scaffold of this article has been released to npm, and developers are also welcome to provide excellent templates.

t-cli documentation

Through this article, we can have these gains:

  • How to design your own scaffolding tools
  • Publish your own npm package
  • Overall framework of scaffold in large front-end field

CLI

What is cli

The explanation given by the search engine is as follows:

Command line interface (English: command line interface, abbreviation: CLI) is the most widely used user interface before the popularity of graphical user interface. It usually does not support the mouse. Users enter instructions through the keyboard and the computer executes them after receiving the instructions. It is also called character user interface (CUI). It is generally believed that the command line interface (CLI) is not as user-friendly as the graphical user interface (GUI)

Why use cli

  • Reduce repetitive work (webpack configuration, directory, routing and other configuration information)
  • Standardize team code style and directory hierarchy (unified eslint and git configuration)
  • Unified plug-in, dependent version, avoiding unknown dependency errors

Basic process

At present, the internal implementation principles of mainstream scaffolds may be different, but the final functions are almost the same. The main functions include:

Project construction

  • Interact with users to generate and obtain project configuration information
  • Download / generate project templates
  • Production and development environment depends on installation

development environment

  • Local development (hot update, interface agent, etc.)
  • eslint code style detection and repair

Project construction

  • build project
  • Project deployment
  • Dependency analysis

Webhook + Jenkins can also be used to realize automatic deployment in this step. By configuring certain trigger rules for the remote warehouse, when a user push es, it will automatically build and deploy.

preparation in advance

Dependent plug-in preparation

  • babel: syntax conversion tool
  • commander: command line tool, through which you can read command line commands
  • inquirer: user computer command line interaction tool
  • Download git repo: git folder Download
  • It is used to modify the log style and output color through the command line of chalk
  • ora: loading effect plug-in
  • gulp build tool

In addition to the above, dependency also includes some other development environment dependencies. For complete reference t-cli-github

Engineering template

Scaffolding can quickly generate project structure and configuration. The most common way is that we prepare a set of general and standardized project templates in advance and store them in the specified location. When scaffolding executes the command to create a project, we can directly copy the prepared templates to the directory. git warehouse is usually used to store templates. First, it is convenient to upgrade and maintain in the later stage. Second, the npm package will not be too large.

npm contracting

Prepare an npm account. If not, please register on the official website.

  • Project initialization: create directory, execute npm init and generate package JSON, where name is the package name. In order to prevent the conflict between your favorite name and the existing package name, you can use scope contracting, such as @ canyuegongzi/t-cli
  • Login: npm login
  • Publish: npm publish. If the package name is non scope, this instruction can be used to publish directly. If it is scope, parameters need to be added. The complete instruction is as follows: npm publish --access=public

So far, we have released an npm package of our own, but the content is empty.

Note: the release package must be under the npm source, and errors will be reported in taobao and cnpm environments

Environment construction

1: Create the project t-cli directory, execute npm init, and generate package JSON, change the name to @ canyuegongzi/t-cli.

The original name is t-cli, but this name has been registered, so we can only use scope.

2: Modify package The bin parameter in JSON points to the entry file.

bin": {
    	"t": "src/cli/bin/t.js"
  },

3: Build gulp build environment

Why gulp is adopted instead of webpack or rollupjs: the construction tool can be used at will. Here, you only need es6 conversion and file copy, and you can choose freely.

const gulp = require('gulp');
const babel = require('gulp-babel');

gulp.task("babel", function () {
    return gulp.src("./src/**/*.js")
        .pipe(babel({
            "presets": [
                "@babel/preset-env"
            ],
            "plugins": [
                "@babel/plugin-proposal-object-rest-spread",
                "@babel/plugin-transform-runtime"
            ]
        }))
        .pipe(gulp.dest("cli_dist"))
})
gulp.task("copy-config", function () {
    return gulp.src(["./src/cli/config/*.json"])
        .pipe(gulp.dest("./cli_dist/cli/config"))
})
gulp.task('default', gulp.series('babel', 'copy-config'));

Modify package JSON scripts script

  "scripts": {
    "build": "gulp default",
    "test": "jest"
  },

Directory construction

├── src                               
|-|- cli                            
|-|- |- bin
|-|- | -|- t.js                  // System entry file
|-|- |- config                              
|-|- | -|- category.json            // System template type configuration information
|-|- | -|- template.json            // System template information
|-|- |- lib                          
|-|- | -|- init.js               // init instruction
|-|- | -|- list.js               // list instruction
|-|- | -|- update.js             // update instruction
|-|- |- utils                  
|-|- | -|- download.js            // File download
|-|- | -|- error.js                    
|-|- | -|- log.js                       
├── test                           // test case
|- .npmignore                       // npm package ignore file
|- .babelrc                        // babel configuration
|- .gulpfile.js                    // gulp configuration
|- package.json                    // Development configuration
|- jest.config.js                   // Test configuration

init instruction

The entry file declares the command line. The entry file must be #/ usr/bin/env node declaration.

Use the commander to set different commands. The command method sets the name of the command, the description method sets the description of the command, the alias method sets the short name of the command, and the options sets the parameters required by the command. View the commander's official website.

Command statement

When the user calls the init < app name > command to create a project template, the callback function create function in the action option will be called.

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

program
    .version(`@canyuegongzi/t-cli ${require('../../../package').version}`)
    .usage('<command> [options]')

// Declare the init command and two parameters - c and - t. the callback function in action is the function that the user needs to execute when calling init
program
    .command('init <app-name>')
    .description('Initialize a project')
    .option('-c, --category <category>', 'Project type,[web | server]')
    .option('-t, --template <template>', 'Template name')
    .action((name, options) => {
        require('../lib/init').create(name, options).then(r => {})
    })

Select template type (web OR server)

When calling the init command, the user needs to select the template type if the - c parameter is not passed in.

When you need to interact with users, you need the inquirer plug-in mentioned earlier. The specific code is as follows:

/**
 * Select project type
 * @returns {Promise<void>}
 */
async function selectCategory() {
    return new Promise(resolve => {
        inquirer.prompt([
            { type: 'list', message: 'please select category:', name: category, choices: categoryList }
        ]).then((answers) => {
            console.log(answers);
            resolve(answers[category])
        })
    })
}

Select template

When the user calls the init command, if the - t (no template is specified) parameter is not passed in, the user needs to select the template.

Before you select a template, you need to filter all templates according to the template type. The specific code is as follows:

/**
 * Select project template name
 * @returns {Promise<void>}
 */
async function selectTemplate(projectCategory) {
    try {
		// Filter templates by template type
        const list = templateList.filter(item => item.type === projectCategory).map((item) => item.name)
        if (!list.length || !list) {
			// Give the user a prompt when there is no template
            return log('WARING', 'no template');
        }
        return new Promise(resolve => {
            inquirer.prompt([
                { type: 'list', message: 'please select template:', name: template, choices: list }
            ]).then((answers) => {
                resolve(answers[template])
            })
        })
    }catch (e){
        log('ERROR', e);
    }

}

Project information collection

Each project has a package JSON, which also needs to be manually input by the user during initialization, and modify the file information through the file system provided by node. In this case, the implementation is relatively simple, and the user only needs to input name, version and description.

The implementation code is as follows:

/**
 * The user enters some configuration information himself
 * @param name
 * @returns {Promise<void>}
 */
async function getUserInputPackageMessage(name) {
    return new Promise(async (resolve, reject) => {
        if(isTest) {
            return resolve({name, author: '', description: '', version: '1.0.0' })
        }
        try {
			// The user is prompted to enter version name and version description
            const messageInfoList = await Promise.all([
                inquirer.prompt([
                    { type: 'input', message: "what's your name?", name: 'author', default: '' },
                    { type: 'input', message: "please enter version?", name: 'version', default: '1.0.0' },
                    { type: 'input', message: "please enter description.", name: 'description', default: '' },
                ])
            ]);
            resolve({...messageInfoList[0], name});
        }catch (e) {
            resolve({name, author: '', description: '', version: '1.0.0' })
        }
    })
}

File download implementation

Here, the previously mentioned ownload git repo plug-in is also used for template download. The specific implementation code is as follows:

/**
 * Download files to directory
 * @param url
 * @param name
 * @param target
 * @returns {Promise<void>}
 */
async function downloadFile(url, name, target = process.cwd()) {
    return new Promise((resolve, reject) => {
        const dir = path.join(target, name);
		// If you have this directory name, delete it directly
        rimraf.sync(dir, {});
        const downLoadCallback = (err) => {
            if (err) {
                resolve({flag: false, dir, name});
                log('ERROR', err);
            }
			// The directory will be returned after the download is successful
            resolve({flag: true, dir, name});
        }
        download(url, dir, {clone: true}, downLoadCallback);
    })

}

init project

When the user invokes the init command, the general process is as follows: first obtain the template information, then download and then modify the file information.

The init process code is implemented as follows:

/**
 * Initialize project template
 * @param pluginToAdd
 * @param options
 * @param context
 * @returns {Promise<void>}
 */
async function init (pluginToAdd, options = {}, context = process.cwd()) {
    let projectCategory = options[category]
    let projectTemplate = options[template]
    let projectName = pluginToAdd;
	// When the user does not transfer parameter -c, the user needs to select the template type
    if (!options.hasOwnProperty(category)){
        projectCategory = await selectCategory()
    }
	// When the user does not transfer parameters -t, the user needs to select a template
    if (!options.hasOwnProperty(template)){
        projectTemplate = await selectTemplate(projectCategory)
    }
	// Obtain the template address according to the template type and template selected by the user
    const templateInfo = templateList.find((item) => item.type === projectCategory && item.name === projectTemplate);
    if (!templateInfo) {
        return log('WARING', 'no template');
    }
    const {url} = templateInfo;
	// Obtain the project information of the project entered by the user
    const packageInfo = await getUserInputPackageMessage(projectName);
	// Start a download icon prompt
    const downloadSpinner = ora({ text: 'start download template...', color: 'blue'}).start();
	// Download to the current template according to the template address
    const {dir, name, flag} = await downloadFile(url[0], projectName, context)
    if (flag) {
		// End download icon after successful download
        downloadSpinner.succeed('download success');
        const editConfigSpinner = ora({ text: 'start edit config...', color: 'blue'}).start();
        // Modify the configuration information after downloading
        const successFlag = await downloadSuccess(dir, name, packageInfo);
        if (successFlag) {
            editConfigSpinner.succeed('create success');
        }else {
            editConfigSpinner.fail('create fail');
        }
    } else {
        downloadSpinner.fail('download fail');
    }
}

After the scaffold project is created, it needs to be modified according to the previously collected project information.

/**
 * Template downloaded successfully
 * @param dir
 * @param name
 * @param packageInfo
 * @returns {Promise<void>}
 */
async function downloadSuccess(dir, name, packageInfo) {
    return new Promise((resolve) => {
		// Read package json
        fs.readFile(dir + '/package.json', 'utf8', (err, data) => {
            if (err) {
                resolve(false);
            }
            const packageFile = {...JSON.parse(data), ...packageInfo}
			// Modify the configuration information and write it again
            fs.writeFile(dir + '/package.json', JSON.stringify(packageFile, null, 4), 'utf8', (err) => {
                if (err) {
                    resolve(false);
                }
                resolve(true);
            });
        })
    })
}

list instruction

Command statement

When the main user of the list command queries the project template supported by the current scaffold, it will call the callback function in the action option. The command supports the - c parameter, and the optional values include web and serve.

program
    .command('list')
    .description('List project templates')
    .option('-c, --category <category>', 'Project type,[web | server]')
    .option('-q, --query <query>', 'Query string')
    .action((options) => {
        require('../lib/list')(options)
    })

Template query

/**
 * List templates
 * @param options
 * @param context
 * @returns {Promise<void>}
 */
async function list (options = {}, context = process.cwd()) {
    let projectCategory = options[category];
    let projectQuery = options[query];
    let templateLogList = templateList;
    if (projectCategory){
		// First filter based on template type
        templateLogList = templateList.filter(item => item.type === projectCategory);
    }
    if (projectQuery) {
		// Query template list by template name
        templateLogList = templateLogList.filter(item => item.name.indexOf(projectQuery) > -1)
    }
	// Print template information
    for (let i = 0; i < templateLogList.length; i ++) {
        const str = `${templateLogList[i].name}`;
        log('TEXT', str );
    }
    if (!templateLogList.length) {
        log('WARING', 'No matching template !!!');
    }
	// End the program after printing
    process.exit(0);
}

update instruction

Command statement

Update is mainly used to update the template list. This command can obtain the latest template without upgrading the scaffold.

program
    .command('update')
    .description('Update configuration')
    .option('-t, --type <type>', 'Update type,[config]')
    .action((options) => {
        require('../lib/update')(options)
    })

Get the latest configuration information

/**
 * Get template
 * @param options
 * @param context
 * @returns {Promise<void>}
 */
async function getList(options = {}, context = process.cwd()) {
    return new Promise(resolve => {
		// Call http service
        https.get(configUrl, (response) => {
            let data = '';
            response.on('data', (chunk) => {
                data += chunk;
            });
            response.on('end', () => {
                resolve(JSON.parse(data));
            });

        }).on("error", (error) => {
            log('ERROR', error.message);
        });
    })
}

Modify profile

This part of the code is a simple file operation through node. I won't explain it one by one, Source code

last

The article is limited in length and cannot explain each line of code. Interested students can clone the code and implement it by themselves.

Through the above content, this paper completely realizes a scaffold with high configuration. This scaffold may not be suitable for each development environment, but through the article, we can sort out the working principle of the scaffold. With a certain foundation, you can slowly expand the function in the later stage.

https://github.com/canyuegongzi/t-cli

Keywords: node.js cli

Added by adige on Fri, 18 Feb 2022 03:09:54 +0200