Introduction to the

This article will configure a Monorepo type component library from scratch, including normalization configuration, packaging configuration, component library documentation configuration and development of some scripts to improve efficiency, etc. Monorepo is not familiar with here a word introduction, is a git repository containing multiple independently published modules/packages.

Ps. This article involves the tool configuration in the usual development actually do not need their configuration, we use all kinds of scaffolding are done for us, but at least we need to know is what meaning, and why, probably speaking ashamed, the author as a three or four years length of the front end of the old man, little do-it-yourself match, not even to understand, Therefore, most of the following tools are used for the first time by the author. In addition to introducing how to configure, I will also talk about some pitfalls and solutions, and try to understand the meaning and principle of each parameter. If you are interested, please continue to read

Manage projects using LERNA

First of all, each component is an independent NPM package, but a component may rely on another component, so that if the component after the bug fixes released a new version, need to manually to rely on its components in each upgrade release again, this is a complicated and inefficient process, so you can use leran tools to manage, Lerna is a tool specifically designed to manage JavaScript projects with multiple packages to help with NPM publishing and Git uploads.

First install lerna globally:

npm i -g lerna
Copy the code

Then go to the warehouse directory and execute:

lerna init
Copy the code

This command is used to create a new lerna repository or upgrade an existing lerna repository. Lerna can be used in two modes:

1. Fixed mode. By default, the version number defined in the version field in lerna.json configuration is used for the major and minor versions of all packages. Then all packages will be upgraded to this version and released. If a single package wants to be released, it can only be released by modifying the revised version number.

2. Standalone mode means that each package uses a separate version number.

The following directories are automatically generated:

You can see there is no.gitignore file, so create it manually and just ignore the node_modules directory for now.

All packages are stored in the Packages folder. To add new packages, use the lerna create XXX command (which will be generated from scripts later). The component library recommends adding a uniform scope to package names to avoid naming conflicts. As of version 2.0, NPM supports distribution of scoped packages. The default scope is your NPM username, for example: @username/package-name, you can also use NPM config set @scope-name:registry http://reg.example.com to associate a scope with the NPM repository you are using.

To add dependencies to packages, run lerna add module-1 –scope=module-2, run lerna add module-1 –scope=module-2

As you can see from the link flag, Lerna Add also performs lerna Bootstrap by default, which installs dependencies for all packages.

Lerna publish does not support the –access public parameter when publishing a package with scope using NPM. In addition, lerna publish does not support this parameter. One solution is to add this to all packages’ package.json files:

{
    // ...
    "publishConfig": {
        "access": "publish"}}Copy the code

Normalized configuration

eslint

Eslint is a configured JavaScript code checking tool that can restrict code styles and detect potential errors so that different developers will have a uniform style of code, such as whether to allow the use of ==, whether to remove the end of statements; And so on, eslint rules very much, can see eslint.bootcss.com/docs/rules/ here.

All esLint rules can be configured separately and are disabled by default, so it is cumbersome to configure them one by one. However, esLint has an inherited configuration that makes it easy to use other people’s configurations.

npm i eslint --save-dev
Copy the code

Then add a command to the package.json file:

{
    "scripts": {
		"lint:init": "eslint --init"}}Copy the code

To create an ESLint configuration file, type NPM run lint:init on the command line. After answering a few questions for your situation, a default configuration will be generated, which looks like this:

Take a quick look at what each field means:

  • Env specifies the environment in which your code is to be run, such as in a browser or node. The global variables are different in different environments.

  • ParserOptions specifies supported language options, such as JavaScript version, whether to enable JSX, etc. Setting the correct language options allows ESLint to determine what parsing errors are;

  • React plugins are a list of plugins. For example, if you use React, you need to use the react plugin to support the react syntax. Since I use Vue, I use the Vue plugin to detect syntax problems in single files. The prefix can be omitted.

  • Rules is a rule configuration list. You can enable or disable a rule.

  • The extends extends is an inheritance described above. It uses the officially recommended configuration and a configuration that comes with the vue plugin. The configuration is usually named eslint-config-xxx, the prefix can be omitted, and the plugin can also provide configuration functionality. The import rule is plugin:plugin-name/ XXX. Other well-known configurations such as eslint-config-airbnb can also be used.

Like.gitignore, esLint can also create an ignore configuration file. Eslintignore, where each line is a glob pattern to indicate which paths to ignore:

node_modules
docs
dist
assets
Copy the code

Then add the command to run the check in package.json:

"scripts": {
    "lint": "eslint ./ --fix"
}
Copy the code

–fix specifies that esLint is allowed to fix, but few problems can be fixed automatically. NPM run lint results in the following:

husky

Currently, you have to run esLint checking manually, and even if you can discipline yourself to check your code every time you commit, it doesn’t necessarily apply to anyone else. A specification that doesn’t enforce it is the same as one that doesn’t, so it’s best to enforce it before git commits, using Husky. This tool allows you to execute certain git commands before executing them. You need to check esLint before committing to git. This requires pre-commit hooks. .

International practice, first install:

npm i husky@4 --save-dev
Copy the code

Then add the following to package.json:

{
    "husky": {
        "hooks": {
            "pre-commit": "npm run lint"}}}Copy the code

Then I tried git commit, but it didn’t work… I checked the node, NPM, and Git versions and found nothing wrong, then opened the hidden git folder.

I found that the current hook files still have the sample suffix at the end. If I want a hook to work, I need to remove the suffix, but this operation should not be done manually, so I have to try reinstalling Husky. After a brief test, I found that the V5.x version does not work either. However, two versions of v3.0.0 and v1.1.1 are effective (the author’s system is windows10, which may be related to the author’s computer environment) :

This will terminate the commit operation if errors are detected, but now another package lint-staged is commonly used. This package, as its name implies, only examines documents that have been passively submitted, while other documents that have not been changed will not be checked. This is reasonable and will speed up the inspection. NPM I lint-staged –save-dev then go to package.json and configure it:

{
    "husky": {
        "hooks": {
            "pre-commit": "lint-staged"}},"lint-staged": {
        "*.{js,vue}": [
            "eslint --fix"]}}Copy the code

First, git hook commands are changed to Lint-staged commands. Lint-staged field values are objects, object keys are glob matched, values can be strings or arrays of strings, and each string represents an executable command. If Lint-staged files are found to be matched, a command corresponding to a rule matching a file will be executed, and the matched file will be passed to the command as a parameter list when executing the command, for example: exlint –fix xxx.js xxx.vue … Eslint –fix xxx.js if a js or vue file is matched in a temporary file, run eslint –fix xxx.js… NPM run lint NPM run lint NPM run lint NPM run lint NPM run lint NPM run lint NPM run lint

In the screenshot above, you can see that there are 14 errors in total, but this time I only modified one file, so I only checked this one file:

stylelint

Stylelint is very similar to ESLint, except that it is used to check CSS syntax. In addition to CSS files, it also supports CSS preprocessor languages such as SCSS and LESS. Stylelint may not be as popular as ESLint, but let’s try it out for learning purposes. After all, the component library must write the style, still install first: NPM I stylelint stylelint-config-standard –save-dev, stylelint-config-standard is the recommended configuration file, like eslint-config-xxx. If you don’t like this rule you can change it to another one, then create a configuration file.

{
  "extends": "stylelint-config-standard"
}
Copy the code

Create an ignore configuration file. Stylelintignore, enter:

node_modules
Copy the code

Finally add a line of command to package.json:

{
	"scripts": {
        "style:lint": "stylelint packages/**/*.{css,less} --fix"}}Copy the code

Check all files in the Packages directory that end in CSS or less and fix them automatically if possible.

Git commit: $package.json: $git commit: $git commit

{
    "lint-staged": {
        "*.{css,less}": [
            "stylelint --fix"]}}Copy the code

commitlint

Git commit content is important to understand what a commit does. The standard format for git commit content consists of three parts: Header, Body and Footer are mandatory. But to be honest, I’m too lazy to write Header, let alone other parts, so I’d better use the tool if I can’t do it consciously. Git commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint: commitLint

npm i --save-dev @commitlint/config-conventional @commitlint/cli
Copy the code

Commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js: commitlint.config.js

module.exports = {
    extends: ['@commitlint/config-conventional']}Copy the code

You can also configure the rules you need separately and then go to the husky section of package.json to configure the hooks:

{
    "husky": {
        "hooks": {
            "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"}}}Copy the code

The commitlint command requires an input parameter, which is the commit message we entered. The -e parameter means the following:

Git /hooks/commit-msg this is the bash script executed by the commit- MSG hook. Git /hooks/commit-msg

$$$$$$$$$$$$$

function run([, scriptPath, hookName = ' ', HUSKY_GIT_PARAMS], getStdinFn = get_stdin_1.default) {
    console.log('block', scriptPath, hookName, HUSKY_GIT_PARAMS)
    
    // ...
}
Copy the code

Let’s print and see what the parameters are:

HUSKY_GIT_PARAMS is the path to the file that holds the contents of the commit message we entered this time, which husky will then set to the environment variable:

const env = {};
if (HUSKY_GIT_PARAMS) {
    env.HUSKY_GIT_PARAMS = HUSKY_GIT_PARAMS;
}
if (['pre-push'.'pre-receive'.'post-receive'.'post-rewrite'].includes(hookName)) {
    // Wait for stdin
    env.HUSKY_GIT_STDIN = yield getStdinFn();
}
if (command) {
    console.log(`husky > ${hookName} (node ${process.version}) `);
    execa_1.default.shellSync(command, { cwd, env, stdio: 'inherit' });
    return 0;
}
Copy the code

Commitlint -e HUSKY_GIT_PARAMS: Commitlint reads the.git/COMMIT_EDITMSG file to check that the commit message we typed is in compliance with the specification.

You can see that we just typed a 1 and we got an error.

commitizen

As mentioned above, a standard COMMIT Message contains three parts, which look like this in detail:

<type>(<scope>): <subject> empty line <body> empty line <footer>Copy the code

When you type git commit, a command line editor comes up that lets you type in git commit, but the editor is awkward and you can’t save it if you don’t use it, so you can use commitizen for interactive input by executing the following commands in sequence:

npm install commitizen -g

commitizen init cz-conventional-changelog --save-dev --save-exact
Copy the code

This should automatically add the following configuration to your package.json file:

{
    "config": {
        "commitizen": {
            "path": "./node_modules/cz-conventional-changelog"}}}Copy the code

You can then use git cz instead of git commit. It will give you some options and ask you a few questions.

The git commit command can be configured to convert git commit to git cz:

{
    "husky": {
        "hooks": {
            "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",}}}Copy the code

But I tried no, the system could not find the specified path. If you know how to fix it, see you in the comments section.

{
    "husky": {
        "hooks": {
            "prepare-commit-msg": "echo ----------------please use [git cz] command instead of [git commit]----------------"}}}Copy the code

For example, prettier can be used for code beautification, and Convention-Changelog or standard-version can be used for generating submitted logs. If necessary, you can try by yourself.

Packaging configuration

The current structure of each component looks like this:

Index.js returns an object with the install method. As a plug-in to vue, this component is used as follows:

import ModuleX from 'module-x'
Vue.use(ModuleX)
Copy the code

If the js file uses the latest syntax, you need to add the following configuration to vue.config.js in the project that uses this component:

{
    transpileDependencies: [
        'module-x']}Copy the code

Since babel-loader ignores all files in node_modules by default, adding this configuration allows Babel to explicitly translate this dependency.

However, if you want to pack and then publish is also available, we will add a package configuration.

Install the related tools first:

npm i webpack less less-loader css-loader style-loader vue-loader vue-template-compiler babel-loader @babel/core @babel/cli @babel/preset-env url-loader clean-webpack-plugin -D
Copy the code

Because more, will not be introduced one by one, should still be more clear, respectively is used to parse style files, VUE single files, JS files and other files, can be added or reduced according to your actual situation.

Package each package separately and output the package results to the dist directory in the respective folder. We use the Node API of WebPack to do this:

// ./bin/buildModule.js

const webpack = require('webpack')
const path = require('path')
const fs = require('fs-extra')
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
const {
    VueLoaderPlugin
} = require('vue-loader')

// Gets the command line argument used to package the specified package, otherwise package all packages in the packages directory
const args = process.argv.slice(2)

// Generate the WebPack configuration
const createConfigList = () = > {
    const pkgPath = path.join(__dirname, '.. / '.'packages')
    // Determine the packet to be typed based on whether an argument was passed
    const dirs = args.length  > 0 ? args : fs.readdirSync(pkgPath)
    // Generate a WebPack configuration for each package
    return dirs.map((item) = > {
        return {
            // The entry file is the index.js file in each package
            entry: path.join(pkgPath, item, 'index.js'),
            output: {
                filename: 'index.js'.path: path.resolve(pkgPath, item, 'dist'),// Pack and delete to folder dist
                library: item,
                libraryTarget: 'umd'.// Package into umD modules
                libraryExport: 'default'
            },
            target: ['web'.'es5'].// By default, webpack5 generates code that includes const, let, arrow functions, etc. Es6 syntax, so you need to set up the code to generate ES5
            module: {
                rules: [{test: /\.css$/,
                        use: ['style-loader'.'css-loader'] {},test: /\.less$/,
                        use: ['style-loader'.'css-loader'.'less-loader'] {},test: /\.vue$/,
                        loader: 'vue-loader'
                    },
                    {
                        test: /\.js$/,
                        loader: 'babel-loader'
                    },
                    {
                        test: /\.(png|jpe? g|gif)$/i,
                        loader: 'url-loader'.options: {
                            esModule: false// The latest version of file-loader uses es module to import images by default. The generated link is an object, so if you import images through require, you cannot access them}}},plugins: [
                new VueLoaderPlugin(),
                new CleanWebpackPlugin()
            ]
        }
    })
}

// Start packing
webpack(createConfigList(), (err, stats) = > {
    // Processing and result processing...
})
Copy the code

Then run node. /bin/ buildmodule. js to type all packages, or node. /bin/ buildmodule. js XXX xxx2… To pack your bag.

Of course, this is only the simplest configuration, and in fact there are certain specific problems, such as:

  • If you rely on other base component libraries, it will be more troublesome. In this case, it is recommended not to package, direct source distribution;

  • If the vue extension is missing when looking for files, configure the resolve.extensions for Webpack.

  • If you are using some relatively new JavaScript syntax or JSX, you need to configure the corresponding Babel plugin or default.

  • Reference vue, jquery and other external libraries, cannot be packaged directly, so we need to configure the externals of webpack.

  • A package may have multiple entries, in other words, individual packages may have specific configurations, so you can add a configuration file under the package, and then the configuration generation code above can read the file for configuration merge;

These problems are not difficult to solve, look at the reported error and then go to search the basic is easy to solve, if you are interested in this article can also go to the source view.

A small optimization, since the webpack packaging is not simultaneous, so the total time is very slow, you can use the parallel-webpack plugin to make it parallel packaging:

npm i parallel-webpack -D
Copy the code

Because its API uses the configuration file path and cannot pass the object type directly, we need to modify the above code to export a configuration:

// Change the file name to config.js

// ...

/ / delete
// webpack(createConfigList(), (err, stats) => {
    // Processing and result processing...
// })

// Add export statements
module.exports = createConfigList()
Copy the code

Create another file:

// run.js

const run = require('parallel-webpack').run
const configPath = require.resolve('./config.js')

run(configPath, {
    watch: false.maxRetries: 1.stats: true
})
Copy the code

Node. /bin/run.js to execute, I simply timed it and saved about half the time.

Component Document Configuration

The component documentation tool uses VuePress, and if you run into webPack version conflicts like I did, you can install it separately in the./docs directory:

cd ./docs
npm init
npm install -D vuepress
Copy the code

The basic configuration of Vuepress is very simple, using the default theme and following the tutorial configuration. We will not go into details here, but will show you how to use the components in packages in the document.

The config.js file is the default configuration file of Vuepress, where packaging options, navigation bar, sidebar and so on are configured. EnhanceApp is the enhancement of the client application, where you can get the Vue instance and do some work for application startup, such as registering components.

En /rate is the documentation for one of the components I added. The documentation and sample contents are in the readme. md file in the folder. Vuepress extends markdown. Therefore, markdown file can be used to contain template, script and style blocks like vue single file, which is convenient for sample development in the document. Components need to be imported and registered in enhanceapp. js file first, so the problem comes. The main entry field in package.json points to a different entry field. For example, the main entry field in package.json points to a different entry field in development:

// package.json

{
	"main": "index.js"
}
Copy the code

Then go to enhanceapp.js to import and register:

import Rate from '@zf/rate'

export default ({
    Vue
}) => {
    Vue.use(Rate)
}
Copy the code

Because we can’t find the package, our package has not been released yet, so we can’t install it directly. What should we do? There should be several ways, such as using NPM link to link the package here, but this is too troublesome. Therefore, I choose to modify the webpack configuration of Vuepress, and make it look in the Packages directory when looking for packages. In addition, I also need to set the alias of @zf. Obviously, @zf is not in our directory.

const path = require('path')

module.exports = {
    chainWebpack: (config) = > {
        // The location of our bag
        const pkgPath = path.resolve(__dirname, '.. /.. /.. / '.'packages')
        // Change webpack's resolve.modules configuration to search for directories when resolving modules, go packages first, then node_modules
        config.resolve
            .modules
            .add(pkgPath)
            .add('node_modules')
        // Modify the alias resolve.alias configuration
        config.resolve
            .alias
            .set('@zf', pkgPath)
    }
}
Copy the code

This way we can use our components normally in Vuepress, and when you are finished you can change the entry field of package.json to the packaged directory:

// package.json

{
	"main": "dist/index.js"
}
Copy the code

Other basic information, navigation bar, sidebar, etc. can be configured according to your needs, the effect is as follows:

Add components using scripts

Now let’s look at the steps involved in adding a component:

1. Select a name for the component to be added and use NPM search XXX to check if it already exists.

2. Create a folder in the Packages directory and create a few basic files, usually by copying and pasting other components and modifying them;

3. Create a document folder in the docs directory and create a readme. md file.

4. Modify config.js for sidebar configuration (if sidebar is configured), modify enhanceapp. js import and registration components;

It’s easy to do this, but it’s tedious, and it’s easy to miss a step. These things are really good for scripts. Let’s implement them.

Initialization work

This is the script we’re going to execute. First of all, it must accept some parameters. For simplicity, we only need to enter a component name, but for further extension purposes, we’ll use the Inquirer to process the command line input. Upon receipt of the entered component name, a check is automatically performed to see if the component exists:

// add.js
const {
    exec
} = require('child_process')
const inquirer = require('inquirer')
const ora = require('ora')// ORA is a command-line loading tool
const scope = '@zf/'// Package scope, if your package has no scope, then no

inquirer
    .prompt([{
            type: 'input'.name: 'name'.message: 'Please enter component name'.validate(input) {
                Asynchronous validation calls this method to tell the Inquirer if the validation is complete
                const done = this.async();
                input = String(input).trim()
                if(! input) {return done('Please enter component name')}const spinner = ora('Checking if package name exists').start()
                exec(`npm search ${scope + input}`.(err, stdout) = > {
                    spinner.stop()
                    if (err) {
                        done('Failed to check for package name, please try again')}else {
                        if (/No matches/.test(stdout)) {
                            done(null.true)}else {
                            done('This package name already exists, please change it')
                        }
                    }
                })
            }
        }
    ])
    .then(answers= > {
    	// After the command is entered, perform other operations
        console.log(answers)
    })
    .catch(error= > {
        // Error handling
    });
Copy the code

The results are as follows:

Using a Template

Next, generate folders and files in the Packages directory automatically. In the “package configuration” section, you can see that a basic package has four files: Js, package.json, index.vue, and style.less, create a template folder in the./bin directory, and then create these four files. You can copy and paste the basic contents into them. The contents of index.js and style.less do not need to be modified, so just copy them to the directory of the new component:

// add.js

const upperCamelCase = require('uppercamelcase')// String - style turn hump
const fs = require('fs-extra')

const templateDir = path.join(__dirname, 'template')// Template path

// This method is called in the inquirer's then method above and takes command line input
const create = ({ name }) = > {
    // Component directory
    const destDir = path.join(__dirname, '.. / '.'packages', name)
    const srcDir = path.join(destDir, 'src')
    // Create directory
    fs.ensureDirSync(destDir)
    fs.ensureDirSync(srcDir)
    // copy index.js and style.less
    fs.copySync(path.join(templateDir, 'index.js'), path.join(destDir, 'index.js'))
    fs.copySync(path.join(templateDir, 'style.less'), path.join(srcDir, 'style.less'))}Copy the code

Vue and package.json contents need to be dynamically injected, such as component names for index.vue and package names for package.json. We can use a very simple json-templater library to inject data in double curly brace interpolation. Take package.json as an example:

// ./bin/template/package.json
{
    "name": "{{name}}"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {},
    "author": ""."license": "ISC"
}
  
Copy the code

Name is the data we want to inject, then read the contents of the template, then inject and render, and finally create the file:

// add.js

const upperCamelCase = require('uppercamelcase')// String - style turn hump
const render = require('json-templater/string')

// Render templates and create files
const renderTemplateAndCreate = (file, data = {}, dest) = > {
    const templateContent = fs.readFileSync(path.join(templateDir, file), {
        encoding: 'utf-8'
    })
    const fileContent = render(templateContent, data)
    fs.writeFileSync(path.join(dest, file), fileContent, {
        encoding: 'utf-8'})}const create = ({ name }) = > {
    // Component directory
    // ...
    / / create a package. The json
    renderTemplateAndCreate('package.json', {
        name: scope + name
    }, destDir)
    // index.vue
    renderTemplateAndCreate('index.vue', {
        name: upperCamelCase(name)
    }, srcDir)
}
Copy the code

At this point, the component directories and files are created, as are the document directories and files, so there is no code attached here.

Using AST

The last two files to be modified are config.js and enhanceapp. js. Although these two files can also be injected into the template as above, considering the frequency of modification of these two files may be relatively frequent, it is not convenient to modify them in the template every time, so we use AST instead. This eliminates the need for template placeholders.

Start with enhanceapp.js, where we import and register each additional component:

import Rate from '@zf/rate'

export default ({
    Vue
}) => {
    Vue.use(Rate)
    console.log(1)}Copy the code

The idea is simple: convert the source code of this file to AST, insert the import statement of the new component after the last import statement, insert the registration statement of the new component between the last vue. use statement and console.log statement, and then convert the source code and write it back into this file. AST related operations can be done using Babel’s toolkit: @babel/ Parser, @babel/traverse, @babel/ Generator, @babel/types.

@babel/parser

Converting source code to an AST is simple:

// add.js
const parse = require('@babel/parser').parse

/ / update enhanceApp. Js
const updateEnhanceApp = ({ name }) = > {
    // Read the contents of the file
    const filePath = path.join(__dirname, '.. / '.'docs'.'docs'.'.vuepress'.'enhanceApp.js')
    const code = fs.readFileSync(filePath, {
        encoding: 'utf-8'
    })
    // Convert to AST
    const ast = parse(code, {
        sourceType: "module"// Because the 'import' syntax is used, it indicates that the code is parsed into module mode
    })
    console.log(ast)
}
Copy the code

A lot of data is generated, so the command line is generally unable to display, you can go to astexplorer.net/ to check, choose @babel/parser.

@babel/traverse

After obtaining AST tree, we need to modify the tree. @babel/traverse is used to traverse and modify tree nodes, which is a relatively troublesome step in the whole process. If you are not familiar with the basic knowledge and operation of AST, we recommend reading this document babel-handbook first.

Let’s write the code to add the import statement from the screenshot parsed above:

// add.js
const traverse = require('@babel/traverse').default
const t = require("@babel/types")// This package is a toolkit for detecting the type of a node, creating new nodes, and so on

const updateEnhanceApp = ({ name }) = > {
    // ...
    
    // The first argument to traverse is an AST object, and the second is an accessor that will be called when certain types of nodes are traversed
    traverse(ast, {
        // This function is executed when the Program node is traversed
        // The first argument to the function is not the node itself, but the path of the node. The path contains information about the relationship between the node and other nodes. Subsequent operations are also performed on the path, to access the node itself, you can visit path.node
        Program(nodePath) {
            let bodyNodesList = nodePath.node.body // This is an array
            // Walk through the node to find the last import node
            let lastImportIndex = -1
            for (let i = 0; i < bodyNodesList.length; i++) {
                if (t.isImportDeclaration(bodyNodesList[i])) {
                    lastImportIndex = i
                }
            }
            // Build the AST node for the import statement to be inserted: import name from @zf/name
            / / the node type and the parameters of the need to can see here: https://babeljs.io/docs/en/babel-types
            // If you are not sure which type to use, you can look at the above https://astexplorer.net/ website to see what the corresponding statement is
            const newImportNode = t.importDeclaration(
                [ t.ImportDefaultSpecifier(t.Identifier(upperCamelCase(name))) ], // name
                t.StringLiteral(scope + name)
            )
            // If there is no import node, insert the import node before the first node
            if (lastImportIndex === -1) {
                let firstPath = nodePath.get('body.0')// Get the path of the first node of the body
                firstPath.insertBefore(newImportNode)// Insert the node before the node
            } else { // If an import node exists, insert the import node after the last import node
                let lastImportPath = nodePath.get(`body.${lastImportIndex}`)
                lastImportPath.insertAfter(newImportNode)
            }
        }
    });
}
Copy the code

Let’s take a look at the code to add vue. use. Since the generated AST tree is too long to take a screenshot, you can open the above website and enter the sample code to see the generated structure:

// add.js

// ...

traverse(ast, {
    Program(nodePath) {},
    
    // Iterate over the ExportDefaultDeclaration node
    ExportDefaultDeclaration(nodePath) {
        let bodyNodesList = nodePath.node.declaration.body.body // Find the body of the arrow function node. There are currently two expression nodes
        // The following logic is basically the same as adding import statements: traverse the node to find the last vue. Use node, and then add a new node
        let lastIndex = -1
        for (let i = 0; i < bodyNodesList.length; i++) {
            let node = bodyNodesList[i]
            // Find the node of type vue. Use
            if (
                t.isExpressionStatement(node) &&
                t.isCallExpression(node.expression) &&
                t.isMemberExpression(node.expression.callee) &&
                node.expression.callee.object.name === 'Vue' &&
                node.expression.callee.property.name === 'use'
            ) {
                lastIndex = i
            }
        }
        // Build a new node: vue.use (name)
        const newNode = t.expressionStatement(
            t.callExpression(
                t.memberExpression(
                    t.identifier('Vue'),
                    t.identifier('use')
                ),
                [ t.identifier(upperCamelCase(name))]
            )
        )
        // Insert a new node
        if (lastIndex === -1) {
            if (bodyNodesList.length > 0) {
                let firstPath = nodePath.get('declaration.body.body.0')
                firstPath.insertBefore(newNode)
            } else {// If the body is empty, call pushContainer on the body node to append the node
                let bodyPath = nodePath.get('declaration.body')
                bodyPath.pushContainer('body', newNode)
            }
        } else {
            let lastPath = nodePath.get(`declaration.body.body.${lastIndex}`)
            lastPath.insertAfter(newNode)
        }
    }
});
Copy the code

@babel/generator

After modifying the AST tree, you can go back to the source code:

// add.js
const generate = require('@babel/generator').default

const updateEnhanceApp = ({ name }) = > {
    // ...
    
    // Generate the source code
    const newCode = generate(ast)
}
Copy the code

The effect is as follows:

As you can see, it is not difficult to perform simple operations with an AST. The key is to be careful and patient and find the right nodes. In addition to the config. Js modification is similar, interested can directly look at the source code.

The NPM run add command can be used to automatically create a new component, which can be directly used for component development

At the end

This article is the author in the transformation of their component library some process records, because it is the first practice, inevitably there will be mistakes or unreasonable place, welcome to point out, thank you for reading, goodbye ~

Example code repository: github.com/wanglin2/vu… .