As a front-end dish 🐔, the daily work is to write all kinds of business code, looking at the big guys write small tools, plug-ins what, envy unceasingly. Why don’t you write a plugin? Just try it. Just try it. Here we go.

Write for the first time, have improper place still hope everybody big guy corrects.

I. Introduction of plug-ins.

auto-export-plugin

Look at the picture

That’s right!

Tangerine: This saves you from having to manually import the index itself, which can improve development efficiency.

That’s right!!!!!


Main Application Scenarios

Plug-in installation


npm i auto-export-plugin -D

Copy the code

Plug-in function

  • When a file is modified, the export statement in the file is automatically collected and written into the index.js file.
  • When a file is deleted, the export statement of the file is automatically deleted from the index file.

If the changes are not in the index.js file, the changes are automatically written to the index.js file in the same directory. Ignored = /index/ ignored = /index/ ignored = /index/ ignored = /index/ index.js = /index/ index.js = /index/ index.js = /index/ index.js

usage

const path = require('path')
const AutoExport = require('auto-export-plugin')
module.exports = {
  entry: './index.js'.output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [{test: /\.js$/.use: 'babel-loader'}},plugins: [
    new AutoExport({
      dir: ['src'.'constants'.'utils']]}})Copy the code

Two, principle analysis

  1. Use Babel to parse the contents of the modified files into AST, and find export through AST
  2. Perform ast conversion on the index.js file of the same directory and insert the collected export
  3. Redo the ast converted from index.js and write it to the index.js file

The plugin has been updated. See the update here

Three, source code interpretation

Preliminary knowledge

An AST is an abstract syntax tree, so every line of code, every character we write can be parsed into an AST.

// test.js
export const AAA = 2
Copy the code

The entire file parses into an AST(leaving out some structure) as follows

{
  "type": "File"
  "program": {..."body": [{// ExportNamedDeclaration isexportstatements"type": "ExportNamedDeclaration"."declaration": {
        "type": "VariableDeclaration"."declarations": [{
          "type": "VariableDeclarator"."id": {
            "type": "Identifier"// name is the exported variable named AAA"name": "AAA". },"init": {
            "type": "NumericLiteral"// value is the value of the exported variable"value": 2... }}]."kind": "const"}}],}},Copy the code

You will notice that each layer of the AST has the same structure as follows, and each layer is called a node

{
    type: 'xxxxx'. }Copy the code

When we import from a file, we only import variable names, such as import {AAA} from ‘./test’. Therefore, we only need to collect all the exported variable names of the current file (e.g., “AAA”), not the exported variable values (e.g., “2”).

My initial idea was to iterate over the body of the AST, use ‘===’ for type determination, and collect variable names when the ExportNamedDeclaration type is encountered.

Later, it was found that there is a more convenient way in the document. There are two hooks for each node: Enter and exit, which are executed when the node is accessed and the node is left. Nodes can be inserted, deleted and replaced in these two hooks.


Source code is here (partially)

Auto-export -plugin plugin

The following code has omitted some snippets and can be interpreted against the full source code.

Gets the exportNames of the modified file

  getExportNames(filename) {
      const ast = this.getAst(filename);
      let exportNameMap = {};
      traverse(ast, {
        // export const a = 1
        ExportNamedDeclaration(path) {
          if(t.isVariableDeclaration(path.node.declaration)) { ... }},Export function getOne(){
         FunctionDeclaration(path) {
          if(t.isExportNamedDeclaration(path.parent)) { ... }},// const A = 1; Export {A}
        ExportSpecifier(path) {
          const name = path.node.exported.name;
          exportNameMap[name] = name;
        },
        // Handle export default. If it is export default, use filename as variable nameExportDefaultDeclaration() { ... }});return exportNameMap;
  }
Copy the code

This will get all the export variable names of the corresponding file pairs.

At present, THERE are only four ways to write export statement (please leave a message if there are other ways). Considering that there may be many variable declaration statements in a file but not all of them are export, the value ofexport const a = 1This method does not handle the type separately like the other three methods. Instead, the ExportNamedDeclaration performs further judgment and processing


Write index file

autoWriteIndex(filepath, isDelete = false) {
    // Find the corresponding dir according to the path of the changed file
    const dirName = path.dirname(filepath);
    const fileName = getFileName(filepath);
    // Go through all the files in this directory. If there is index.js, use ast to convert it.
    // If not, create index.js and write
    fs.readdir(dirName, {
      encoding: 'utf8',
    }, (err, files) => {
      let existIndex = false;
      if(! err) { files.forEach(file= > {
          if (file === 'index.js') {
            existIndex = true; }});if(! existIndex) { ... let importExpression =`import { ${exportNames.join(', ')} } from './${fileName}'`; . const data =`
            ${importExpression}\n
            export default {
              The ${Object.values(nameMap).join(', \n')}
            }
          `;
          fs.writeFileSync(`${dirName}/index.js`, data);
        } else {
        // Write exportName by converting the ast of index.js
          this.replaceContent(`${dirName}/index.js`, filepath, nameMap); }}}); }Copy the code

Replace, insert, or delete the ast of the index.js file if it exists

replaceContent(indexpath, filePath, nameMap) {
    ...
    traverse(indexAst, {
        Program(path) {
          const first = path.get('body.0');
          // Since js syntax requires that import statements be written at the top of the file,
          // So if the first statement in the index.js file is not an import statement, there is no import in the current file
          // The import statement needs to be created and inserted into the first statement of the file
          if(! t.isImportDeclaration(first)) {const specifiers = self.createImportSpecifiers(nameMap);
            path.unshiftContainer('body', self.createImportDeclaration(specifiers)(relPath));
          }
          // If the export default statement does not exist, create and insert it under the body
          const bodys = path.get('body')
          if(! bodys.some(item= > t.isExportDefaultDeclaration(item))) {
            path.pushContainer('body', self.createExportDefaultDeclaration(Object.values(nameMap)))
          }
        },
        ImportDeclaration: {
          enter(path) {
            if(! firstImportKey) { firstImportKey = path.key; }// If there is an import statement that changes the file, such as the test file, index contains' import {xx} from './test' '
            // to replace the original import variable name
            if(path.node.source.value === relPath && ! importSetted) {// Record the old export variable name. There are two functions of the record here
             // 1. Compare the old exportName with the new exportName. If they are the same, do not modify the index.js file.
             // 2. In the following ExportDefaultDeclaration statement, you need to change the old exportNames
             // Delete all export statements (because some export statements may have been deleted or renamed in the original file) and add the new exportName to export default.
              oldExportNames = path.node.specifiers.reduce((prev, cur) = > {
                if (t.isImportSpecifier(cur) || t.isImportDefaultSpecifier(cur)) {
                  return [...prev, cur.local.name];
                }
                returnprev; } []); importSetted =true
              path.replaceWith(self.createImportDeclaration(specifiers)(relPath));
            }
          },
          exit(path) {
            After each ImportDeclaration completes Enter, if the internal logic of Enter is not entered, say
            // The current node is not an import statement, so check whether the next node is an import statement.
            // If so, proceed to enter the next import statement and continue;
            // If not, there is no import statement in the current index.js file. Insert an import statement after it
            const pathKey = path.key;
            const nextNode = path.getSibling(pathKey + 1);
            if(! importSetted && ! _.isEmpty(nameMap) && nextNode && ! t.isImportDeclaration(nextNode)) { ... path.insertAfter(self.createImportDeclaration(specifiers)(relPath)); } } }, ExportDefaultDeclaration(path) {/ / write export default will visit ExportDefaultDeclaration again, add exportSetted judgment to prevent causing death cycle
          if(changed && ! exportSetted && t.isObjectExpression(path.node.declaration)) { ... path.replaceWith(self.createExportDefaultDeclaration(allProperties)); }}}); . const output = generator(indexAst); fs.writeFileSync(indexpath, output.code); }Copy the code


The optimized part of the code

  • Caches the name of the variable exported from the modified filethis.cacheExportNameMap = {};In this way, if the file changes are not export-related changes (for example, a new function or variable is defined but not exported), the index file will not be converted.


Four,

In the process of writing plug-ins, packaging and releasing NPM, I encountered many problems that I could not meet when writing business code at ordinary times. I also learned more about Webpack and Node, including releasing NPM. Not a waste of time.

The following articles will summarize the problems encountered, please look forward to it.

Read here, if you have some help, please give a star, thank you 😄, welcome to issue and PR.

Good don’t say first, a lot of demand is waiting, I should write 😄.


Changes have been made in the source section, see here

Reference documentation

  • Babel Plugin Handbook