Introduction: In this paper, we use the idea of scope chain to implement the load on demand component in babel-Plugin. The idea is to do this in a uniform way, by going to the ImportDeclaration, collecting dependencies, generating new nodes, and finally directly replacing all reference names of the modified specifiers[] bindings with the scope chain. Using the same scope chain, you can know if a node is referenced in the context and delete invalid nodes if there is no reference. Even the final replacement of the original node is loaded on demand. This article will walk you through the complete process of implementing babel-plugin-import on demand, and lift the veil of the industry’s acceptance of the Babel plug-in.

Babel-plugin-import: ant-design/babel-plugin-import

Since I already covered Babel and the babel-Plugin in my last article, I won’t go into this article and get straight to the point. For more information about students, please click this link. As we all know, paoding is divided into three stages:

  • In the first stage, when Paoding just began to kill the cattle, he did not understand the structure of the cattle body and saw only the whole cattle.
  • In the second stage, after three years, he sees the inside of the cow, not the whole cow anymore.
  • The third stage, when you kill the cow now, you just touch the body of the cow with your mind and don’t have to look with your eyes.

Now go through the babel-plugin-import plug-in source code step by step in these three stages

Step1: The beginning of the solution of the cattle, see nothing but cattle

First of all,babel-plugin-importIt is to solve the problem that the external components or function libraries referenced in the project are fully packaged during the packaging process, resulting in a large package capacity after compilation, as shown in the following figure: babel-plugin-importThe plug-in source code consists of two files

  • The Index file is the file that the plug-in entry initializes, and it is also the file I highlighted in Step1
  • The Plugin file contains a set of methods for handling the various AST nodes, exported as a Class

Start with the plugin’s entry file Index:

import Plugin from './Plugin';
export default function({ types }) {
  let plugins = null;
  /** * the Program entry initializes the data structure of the plug-in options */
  const Program = {
    enter(path, { opts = {} }) {
      assert(opts.libraryName, 'libraryName should be provided');
      plugins = [
        new Plugin(
          opts.libraryName,
          opts.libraryDirectory,
          opts.style,
          opts.styleLibraryDirectory,
          opts.customStyleName,
          opts.camel2DashComponentName,
          opts.camel2UnderlineComponentName,
          opts.fileName,
          opts.customName,
          opts.transformToDefaultImport,
          types,
        ),
      ];
      applyInstance('ProgramEnter'.arguments.this);
    },
    exit() {
      applyInstance('ProgramExit'.arguments.this); }};const ret = {
    visitor: { Program }, // Initializes the entry to the entire AST tree
  };
  return ret;
}
Copy the code

First, the Plugin is imported into the Index file, and there is a default export function that takes a deconstructed parameter called types, which is deconstructed from the Babel object, which stands for @babel/types, and is used to process the set of methods on the AST node. After being introduced in this way, we do not need to manually introduce @babel/types. After entering the function, you can see that an AST node Program is initialized in the observer. Here, the Program node is processed using a complete plug-in structure, including enter and exit events. Note that:

Generally we abbreviate Identifier() {… } is Identifier: {enter() {… }}.

Some of you might say what is the Program node? See AST tree for const a = 1 below

{
  "type": "File"."loc": {
    "start":... ."end":... },"program": {
    "type": "Program".// Program location
    "sourceType": "module"."body": [{"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
              "type": "Identifier"."name": "a"
            },
            "init": {
              "type": "NumericLiteral"."value": 1}}]."kind": "const"}]."directives": []},"comments": []."tokens": [...]. }Copy the code

Program is equivalent to a root node, a complete source tree. Generally, operations such as data initialization are performed when entering this node, which can also be interpreted as the node that executes before other nodes, and is also the last node to execute exit. Some “cleaning up” can also be done during exit. Since the babel-plugin-import Program node contains the complete structure, there must be some very necessary things to do in exit, which we will discuss later. Let’s look at Enter, where the state parameter is used to construct the user-specified plug-in parameter and verify that the required libraryName exists. Plugin introduced by the Index file is a class structure, so it needs to be instantiated and all parameters of the Plugin and @babel/types are passed in. The Plugin class will be explained below. The applyInstance function is then called:

export default function({ types }) {
  let plugins = null;
  /** * inherits the method from the class and uses apply to change the this pointer and pass the path, state argument */
  function applyInstance(method, args, context) {
    for (const plugin of plugins) {
      if(plugin[method]) { plugin[method].apply(plugin, [...args, context]); }}}const Program = {
    enter(path, { opts = {} }){... applyInstance('ProgramEnter'.arguments.this); },... }}Copy the code

The main purpose of this function is to inherit the methods in the Plugin class and it takes three arguments

  1. Method (String) : The name of the method you need to inherit from the Plugin class
  2. Args: (Arrray) : [Path, State]
  3. PluginPass (Object) : Content is consistent with State, ensuring that the content is passed to the latest State

The main purpose is to have the ProgramEnter inherit the ProgramEnter method of the Plugin class and pass the path and state parameters to ProgramEnter. Similarly, Program exit inherits the ProgramExit method.

Now enter the Plugin class:

export default class Plugin {
  constructor(
    libraryName,
    libraryDirectory,
    style,
    styleLibraryDirectory,
    customStyleName,
    camel2DashComponentName,
    camel2UnderlineComponentName,
    fileName,
    customName,
    transformToDefaultImport,
    types, // babel-types
    index = 0./ / marker
  ) {
    this.libraryName = libraryName; / / the library
    this.libraryDirectory = typeof libraryDirectory === 'undefined' ? 'lib' : libraryDirectory; / / package path
    this.style = style || false; // Whether to load style
    this.styleLibraryDirectory = styleLibraryDirectory; // style package path
    this.camel2DashComponentName = camel2DashComponentName || true; // Whether the component name is converted as a "-" link
    this.transformToDefaultImport = transformToDefaultImport || true; // Handle default imports
    this.customName = normalizeCustomName(customName); // A function or path to process the result of the transformation
    this.customStyleName = normalizeCustomName(customStyleName); // A function or path to process the result of the transformation
    this.camel2UnderlineComponentName = camel2UnderlineComponentName; // process to a form like time_picker
    this.fileName = fileName || ' '; // Link to a specific file, such as antd/lib/button/[abc.js]
    this.types = types; // babel-types
    this.pluginStateKey = `importPluginState${index}`; }... }Copy the code

Plugin has initialized the Plugin’s parameters by passing constructor. All values except libraryName have default values. Note that the customeName and customStyleName parameters in the parameter list can receive a function or an incoming path, so normalizeCustomName needs to be unified.

function normalizeCustomName(originCustomName) {
  if (typeof originCustomName === 'string') {
    const customeNameExports = require(originCustomName);
    return typeof customeNameExports === 'function'
      ? customeNameExports
      : customeNameExports.default;// Import {default:func()} if customeNameExports is not a function
  }
  return originCustomName;
}
Copy the code

This function is used to handle when the argument is a path, perform the conversion and retrieve the corresponding function. If customeNameExports after treatment is still not function import customeNameExports. Default, here be involved in the export default is a little knowledge of syntactic sugar.

export default something() {}
/ / equivalent
function something() {}
export ( something as default )

Copy the code

Regression code, Step1 entry file ProgramEnter inherits Plugin’s ProgramEnter method

export default class Plugin {
  constructor(.){... }getPluginState(state) {
    if(! state[this.pluginStateKey]) {
      // eslint-disable-next-line no-param-reassign
      state[this.pluginStateKey] = {}; // Initialize the tag
    }
    return state[this.pluginStateKey]; // return identifier
  }
  ProgramEnter(_, state) {
    const pluginState = this.getPluginState(state);
    pluginState.specified = Object.create(null); // Import a collection of objects
    pluginState.libraryObjs = Object.create(null); // Collection of library objects (non-module imports)
    pluginState.selectedMethods = Object.create(null); // store the node after importMethod
    pluginState.pathsToRemove = []; // Stores the node to be deleted
    /** * state * state:{* importPluginState "Number" : { * specified:{}, * libraryObjs:{}, * select:{}, * pathToRemovw:[] * }, * opts:{ * ... *}, *... *} * /}... }Copy the code

In ProgramEnter, getPluginState** is used to initialize the importPluginState object in the state structure. The getPluginState function appears very frequently in subsequent operations, so readers need to pay attention to the function. This will not be repeated in the following article. But why do you need to initialize such a structure? This brings us to the idea of plug-ins. As described in the opening flowchart, babel-plugin-import implements on-demand loading by collecting node data after passing through the import node, and then performing the on-demand transformation from all nodes that may reference the import binding. State is a reference type, and the operation on it will affect the initial state value of subsequent nodes. Therefore, the Program node is used to initialize the collection dependent object when entering to facilitate subsequent operations. The method responsible for initializing the state node structure and fetching data is getPluginState. This idea is very important and runs through all the code and purposes that follow, so make sure you understand it and read on.

Step2: after three years, have not seen whole cow also

Through Step1, we have learned that the plug-in inherits ProgramEnter from Program and initializes Plugin dependency. If readers still have some parts that are not clear, please go back to Step1 to digest the content carefully and then continue reading. First, we return to the outer Index file. Previously, only the Program node was registered in the observer mode. There are no other AST node entries, so we need to inject at least the AST node type ImportDeclaration of the import statement

export default function({ types }) {
  let plugins = null;
  function applyInstance(method, args, context) {... }const Program = {
      ...
   }
  const methods = [ // Register an array of AST types
    'ImportDeclaration' 
  ]
  
  const ret = {
    visitor: { Program }, 
  };
  
  // Iterate over the number group, using applyInstance to inherit the corresponding method
  for (const method of methods) { 
    ret.visitor[method] = function() {
      applyInstance(method, arguments, ret.visitor); }; }}Copy the code

Create an array and put in the ImportDeclaration, iterate through the applyInstance_ _ and Step1, and the visitor will have the following structure

visitor: {
  Program: { enter: [Function: enter], exit: [Function: exit] },
  ImportDeclaration: [Function],
}
Copy the code

Now return to the Plugin and enter the ImportDeclaration

export default class Plugin {
  constructor(.){... }ProgramEnter(_, state){... }/** * main target, collect dependencies */
  ImportDeclaration(path, state) {
    const { node } = path;
    // Path may be deleted by the previous instance
    if(! node)return;
    const {
      source: { value }, // Get the library name introduced in the AST
    } = node;
    const { libraryName, types } = this;
    const pluginState = this.getPluginState(state); // Get the structure initialized at Program
    if (value === libraryName) { // If the AST library name is consistent with the plug-in parameter name, the dependency collection will be carried out
      node.specifiers.forEach(spec= > {
        if (types.isImportSpecifier(spec)) { Import is the import of the namespace or the default import
          pluginState.specified[spec.local.name] = spec.imported.name; 
          // Save as: {alias: component name} structure
        } else {
          pluginState.libraryObjs[spec.local.name] = true;// Namespace import or default import is set to true}}); pluginState.pathsToRemove.push(path);// The value of the node is added to the pre-delete array}}... }Copy the code

ImportDeclaration collects dependent fields in import, set to {alias: true} for namespace import or default import, and set to {alias: component name} for destruct import. The getPluginState method is described in Step1. The AST node structure of import using babel-plugin for on-demand loading is explained in detail and will not be covered in this article. After execution, the pluginState structure is as follows

Import {Input, Button as Btn} from 'antd'{... importPluginState0: { specified: { Btn : 'Button', Input : 'Input' }, pathToRemove: { [NodePath] } ... }... }Copy the code

At this point, the state.importpluginState structure has collected all the dependency information that will help the node make the transition later. Everything is ready now, but the wind is blowing. What is east wind? Is the action that gets the transformation import work started. While dependencies are collected in the babel-Plugin implementation of load on Demand, node transformations and old nodes are removed. Everything happens in the ImportDeclaration node. The idea behind babel-plugin-import is to find all AST nodes that might be referenced to import and process them all. ** Some readers might be tempted to convert JSX nodes that reference the import binding, but it doesn’t make much sense to convert JSX nodes because there are probably enough AST node types that reference the import binding. All AST node types that need to be converted should be narrowed as much as possible. Moreover, Babel’s other plug-ins will convert our JSX nodes to other AST types, so we can ignore the AST tree of JSX type and wait for the other Babel plug-ins to convert before doing the replacement. There are a number of possible next entrances, but I’ll start with the most familiar one: React.createElement.

class Hello extends React.Component {
    render() {
        return <div>Hello</div>}}/ / after the transformation

class Hello extends React.Component {
    render(){
        return React.createElement("div".null."Hello")}}Copy the code

After JSX transformation, the AST type is CallExpression (expression for function execution). The structure is shown as follows. It is convenient for students to have a deeper understanding of the following steps after familiarization with the structure.

{
  "type": "File"."program": {
    "type": "Program"."body": [{"type": "ClassDeclaration"."body": {
          "type": "ClassBody"."body": [{"type": "ClassMethod"."body": {
                "type": "BlockStatement"."body": [{"type": "ReturnStatement"."argument": {
                      "type": "CallExpression".// This is the starting point for processing
                      "callee": {
                        "type": "MemberExpression"."object": {
                          "type": "Identifier"."identifierName": "React"
                        },
                        "name": "React"
                      },
                      "property": {
                        "type": "Identifier"."loc": {
                          "identifierName": "createElement"
                        },
                        "name": "createElement"}},"arguments": [{"type": "StringLiteral"."extra": {
                          "rawValue": "div"."raw": "\"div\""
                        },
                        "value": "div"
                      },
                      {
                        "type": "NullLiteral"
                      },
                      {
                        "type": "StringLiteral"."extra": {
                          "rawValue": "Hello"."raw": "\"Hello\""
                        },
                        "value": "Hello"}}]]."directives": []}}]}}Copy the code

So we go to the CallExpression node and continue the transformation process.

export default class Plugin {
  constructor(.){... }ProgramEnter(_, state){... }ImportDeclaration(path, state){... }CallExpression(path, state) {
    const { node } = path;
    constfile = path? .hub? .file || state? .file;const { name } = node.callee;
    const { types } = this;
    const pluginState = this.getPluginState(state);
    // Handle general call expressions
    if (types.isIdentifier(node.callee)) {
      if (pluginState.specified[name]) {
        node.callee = this.importMethod(pluginState.specified[name], file, pluginState); }}/ / handle the React. The createElement method
    node.arguments = node.arguments.map(arg= > {
      const { name: argName } = arg;
      // Determine whether the binding of the scope is import
      if (
        pluginState.specified[argName] &&
        path.scope.hasBinding(argName) &&
        types.isImportSpecifier(path.scope.getBinding(argName).path)
      ) {
        return this.importMethod(pluginState.specified[argName], file, pluginState); // Replacing the reference, the help/import plug-in returns the node type and name
      }
      returnarg; }); }... }Copy the code

As you can see, the source code calls importMethod twice, which triggers import conversion to an on-demand action and returns a brand new AST node. Because import is transformed, the component name we manually imported will not be the same as the transformed name, so importMethod needs to return the transformed new name (an AST structure) to our corresponding AST node, replacing the old component name. The function source code will be examined in detail later. Going back to the original question, why does CallExpression need to call importMethod? Because the two are different in meaning, there are two cases of CallExpression node:

  1. The first case, as analyzed earlier, is the react.createElement after the JSX code has been transformed
  2. The same goes for the AST that we use to manipulate code like function callsCallExpressionType, for example:
import lodash from 'lodash'

lodash(some values)
Copy the code

Therefore, in CallExpression, it first determines whether the value of Node. callee is Identifier or not, and if it is correct, it is the second case described, and then directly transforms. . If not, it is the React createElement method form, traverse the React. The three parameters of the createElement method to take out the name, and then to determine whether the name previously state. PluginState collect the import of the name, Finally, check the scope of name and trace back whether the binding of name is an import statement. These criteria are used to avoid incorrect modification of the original semantics of the function, and to prevent incorrect modification of variables with the same name in the block-level scope due to features such as closures. If all of the above conditions are met, it must be an import reference that needs to be processed. To proceed to the importMethod conversion function, importMethod takes three arguments: component name, File (path.sub.file), and pluginState

import { join } from 'path';
import { addSideEffect, addDefault, addNamed } from '@babel/helper-module-imports';

 export default class Plugin {
   constructor(.){... }ProgramEnter(_, state){... }ImportDeclaration(path, state){... }CallExpression(path, state){... }// Component original name, sub.file, import dependency
   importMethod(methodName, file, pluginState) {
    if(! pluginState.selectedMethods[methodName]) {const { style, libraryDirectory } = this;
      const transformedMethodName = this.camel2UnderlineComponentName // Convert component names based on parameters
        ? transCamel(methodName, '_')
        : this.camel2DashComponentName
        ? transCamel(methodName, The '-')
        : methodName;
       /** * Convert the path according to the user-defined customName first, if not provided, concatenate the path as usual */
      const path = winPath(
        this.customName
          ? this.customName(transformedMethodName, file)
          : join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName), // eslint-disable-line
      );
      /** * The final path is processed depending on whether it is introduced by default. Namespace is not processed */
      pluginState.selectedMethods[methodName] = this.transformToDefaultImport // eslint-disable-line
        ? addDefault(file.path, path, { nameHint: methodName })
        : addNamed(file.path, methodName, path);
      if (this.customStyleName) { // Import the style file according to the path specified by the user
        const stylePath = winPath(this.customStyleName(transformedMethodName));
        addSideEffect(file.path, `${stylePath}`);
      } else if (this.styleLibraryDirectory) { // Imports style files based on the user-specified style directory
        const stylePath = winPath(
          join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName),
        );
        addSideEffect(file.path, `${stylePath}`);
      } else if (style === true) {  / / the introduction of SCSS/less
        addSideEffect(file.path, `${path}/style`);
      } else if (style === 'css') { / / into the CSS
        addSideEffect(file.path, `${path}/style/css`);
      } else if (typeof style === 'function') { // If it is a function, it is generated based on the return value
        const stylePath = style(path, file);
        if(stylePath) { addSideEffect(file.path, stylePath); }}}return{... pluginState.selectedMethods[methodName] }; }... }Copy the code

Once in the function, before you look at the code, notice that two packages are introduced: Path. join and @babel/helper-module-imports were introduced to deal with the need to load paths quickly and concatenated on demand. As for import statement transformations, a new import AST node must be created to load paths on demand. Finally, delete the old import statement. The new import node is generated using @babel/helper-module-imports maintained by Babel. Now to continue the process, first ignore the initial if condition statement, which I’ll explain later. Here are a few more steps that need to be handled in the import handler:

  • The default conversion is in the form of a “-” concatenation word, for example: DatePicker is converted to date-picker, and the function that handles the conversion is transCamel.
function transCamel(_str, symbol) {
  const str = _str[0].toLowerCase() + _str.substr(1); // First convert to a small hump to get the full word
  return str.replace(/([A-Z])/g, $1= >`${symbol}The ${$1.toLowerCase()}`); 
  // datePicker, the re grabs P and precedes it with the specified symbol
}
Copy the code
  • To the specific path where the component resides, and if the plug-in user has given a custom path, use customName.babel-plugin-importWhy not provide the form of the object as an argument? Because the customName modification is based on the transformedMethodName value and passed to the plug-in consumer, the design can more accurately match the path that needs to be loaded on demand. The function that handles these actions is withPath, which is mainly compatible with Linux operating systems and converts the ” supported by Windows file systems to a ‘/’.
function winPath(path) {
  return path.replace(/\\/g.'/'); 
  / / compatible path: Windows default '\', also support the '/', but Linux doesn't support '\', hence unified into '/'
}
Copy the code
  • Evaluate transformToDefaultImport. This option is set to true by default. The transformed AST node is exported by default. We then use @babel/helper-module-imports to generate a new import node. The ** function returns the default Identifier of the new import node instead of the importMethod node. All nodes that reference the old Import binding are replaced with nodes of the newly generated Import AST.

  • Finally, depending on whether the user turns on style on demand, additional processing is introduced with customStyleName or not with style path, as well as parameters such as styleLibraryDirectory (style package path) to handle or generate corresponding CSS load nodes on demand.

So far, one of the most basic conversion lines has been converted, and you have seen the basic conversion flow of loading on demand. Back to the if statement at the beginning of the importMethod function, which is relevant to the task we will perform in step3. Now let’s go to step3.

Step3: fang today of time, minister with god encounter but not with visual, official know check and god desire to go

The last two steps of the load on demand transformation are performed in step3:

  1. It’s not just JSX syntax that introduces import bindings, but other types like ternary expressions, class inheritance, operations, judgment statements, return syntax, etc., that we have to deal with to make sure that all references are bound to the latest import, This would also cause the importMethod function to be called again, but we certainly don’t want the import function to be referenced n times, generating n new import statements, and hence the previous judgment statements.
  2. Enter at the beginningImportDeclarationWhen we collected the information, we only carried out the dependency collection, and did not delete the node. And we haven’t added the action done by the Program node exit

We will list all the AST nodes that need to be dealt with in this way, and give the corresponding Interface and examples for each node (regardless of semantics) :

MemberExpression

MemberExpression(path, state) {
    const { node } = path;
    const file = (path && path.hub && path.hub.file) || (state && state.file);
    const pluginState = this.getPluginState(state);
    if(! node.object || ! node.object.name)return;
    if (pluginState.libraryObjs[node.object.name]) {
      // antd.Button -> _Button
      path.replaceWith(this.importMethod(node.property.name, file, pluginState));
    } else if (pluginState.specified[node.object.name] && path.scope.hasBinding(node.object.name)) {
      const { scope } = path.scope.getBinding(node.object.name);
      // Global variable handling
      if (scope.path.parent.type === 'File') {
        node.object = this.importMethod(pluginState.specified[node.object.name], file, pluginState); }}}Copy the code

MemberExpression (attribute MemberExpression), the interface is as follows

interface MemberExpression {
    type: 'MemberExpression';
    computed: boolean;
    object: Expression;
    property: Expression;
}
/** * console.log(lodash.fill()) * antd.button */
Copy the code

If the plugin option does not turn transformToDefaultImport off, the importMethod method is called and the new node value given by @babel/helper-module-imports is returned. Otherwise, it will determine whether the current value is part of the import information collected and whether it is a global variable in the File scope. By obtaining the scope, it can check whether the parent node is of type File to avoid incorrect replacement of other variables with the same name, such as closure scenarios.

VariableDeclarator

VariableDeclarator(path, state) {
   const { node } = path;
   this.buildDeclaratorHandler(node, 'init', path, state);
}
Copy the code

VariableDeclarator (variable declaration), which is very convenient to understand the processing scenario, mainly deals with const/let/var declarations

interface VariableDeclaration : Declaration {
    type: "VariableDeclaration";
    declarations: [ VariableDeclarator ];
    kind: "var" | "let" | "const";
}
/* const foo = antd */
Copy the code

In this example, the buildDeclaratorHandler method, which ensures that the attribute passed is of the underlying Identifier type and is a reference to the import binding, goes into the importMethod for conversion and returns a new node to override the original attribute.

buildDeclaratorHandler(node, prop, path, state) {
    const file = (path && path.hub && path.hub.file) || (state && state.file);
    const { types } = this;
    const pluginState = this.getPluginState(state);
    if(! types.isIdentifier(node[prop]))return;
    if (
      pluginState.specified[node[prop].name] &&
      path.scope.hasBinding(node[prop].name) &&
      path.scope.getBinding(node[prop].name).path.type === 'ImportSpecifier'
    ) {
      node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState); }}Copy the code

ArrayExpression

ArrayExpression(path, state) {
    const { node } = path;
    const props = node.elements.map((_, index) = > index);
    this.buildExpressionHandler(node.elements, props, path, state);
  }
Copy the code

ArrayExpression (ArrayExpression), the interface is shown below

interface ArrayExpression {
    type: 'ArrayExpression';
    elements: ArrayExpressionElement[];
}
/* [Button, Select, Input] */
Copy the code

Element is an array, and the references we need to convert are array elements. So the props we pass are pure arrays like [0, 1, 2, 3], so we can get data from Elements. The specific transformation method used here is the buildExpressionHandler, which will be frequent in subsequent AST node processing

buildExpressionHandler(node, props, path, state) {
    const file = (path && path.hub && path.hub.file) || (state && state.file);
    const { types } = this;
    const pluginState = this.getPluginState(state);
    props.forEach(prop= > {
      if(! types.isIdentifier(node[prop]))return;
      if (
        pluginState.specified[node[prop].name] &&
        types.isImportSpecifier(path.scope.getBinding(node[prop].name).path)
      ) {
        node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState); }}); }Copy the code

After iterating through the props method and making sure that the properties passed are of the basic Identifier type and are references to the import binding, we enter the importMethod method for conversion, similar to the buildDeclaratorHandler method. Just props is an array

LogicalExpression

  LogicalExpression(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['left'.'right'], path, state);
  }
Copy the code

LogicalExpression (logical operator expression)

interface LogicalExpression {
    type: 'LogicalExpression';
    operator: '| |' | '&';
    left: Expression;
    right: Expression;
}
/** ** handles similarly: * antd && 1 */
Copy the code

It takes the variables on the left and right sides of the logical operator expression and converts them using the buildExpressionHandler method

ConditionalExpression

ConditionalExpression(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['test'.'consequent'.'alternate'], path, state);
  }
Copy the code

ConditionalExpression

interface ConditionalExpression {
    type: 'ConditionalExpression';
    test: Expression;
    consequent: Expression;
    alternate: Expression;
}
/** * handle similar to: * antd? antd.Button : antd.Select; * /
Copy the code

It mainly takes out elements similar to ternary expressions and converts them with buildExpressionHandler method.

IfStatement

IfStatement(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['test'], path, state);
    this.buildExpressionHandler(node.test, ['left'.'right'], path, state);
  }
Copy the code

IfStatement (if statement)

interface IfStatement {
    type: 'IfStatement'; test: Expression; consequent: Statement; alternate? : Statement; }/* if(antd){} */
Copy the code

This node is relatively special, but I don’t see why I should call buildExpressionHandler twice, since there are other AST entries that can handle all the possibilities I can think of. Hope to know the reader can carry out popular science.

ExpressionStatement

ExpressionStatement(path, state) {
    const { node } = path;
    const { types } = this;
    if (types.isAssignmentExpression(node.expression)) {
      this.buildExpressionHandler(node.expression, ['right'], path, state); }}Copy the code

(ExpressionStatement)

interface ExpressionStatement {
    type: 'ExpressionStatement'; expression: Expression; directive? :string;
}
/* module. Export = antd */
Copy the code

ReturnStatement

ReturnStatement(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['argument'], path, state);
  }
Copy the code

Return statement (return statement)

interface ReturnStatement {
    type: 'ReturnStatement';
    argument: Expression | null;
}
/* return lodash */
Copy the code

ExportDefaultDeclaration

ExportDefaultDeclaration(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['declaration'], path, state);
  }
Copy the code

ExportDefaultDeclaration (Export default module)

interface ExportDefaultDeclaration {
    type: 'ExportDefaultDeclaration';
    declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
}
/* return lodash */
Copy the code

BinaryExpression

BinaryExpression(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['left'.'right'], path, state);
  }
Copy the code

BinaryExpression (binary operator expression)

interface BinaryExpression {
    type: 'BinaryExpression';
    operator: BinaryOperator;
    left: Expression;
    right: Expression;
}
/* antd > 1 */
Copy the code

NewExpression

NewExpression(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['callee'.'arguments'], path, state);
  }
Copy the code

NewExpression (new expression)

interface NewExpression {
    type: 'NewExpression';
    callee: Expression;
    arguments: ArgumentListElement[];
}
/* new Antd() */
Copy the code

ClassDeclaration

ClassDeclaration(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['superClass'], path, state);
  }
Copy the code

ClassDeclaration

interface ClassDeclaration {
    type: 'ClassDeclaration';
    id: Identifier | null;
    superClass: Identifier | null;
    body: ClassBody;
}
* class emaple extends Antd {... } * /
Copy the code

Property

Property(path, state) {
    const { node } = path;
    this.buildDeclaratorHandler(node, ['value'], path, state);
  }
Copy the code

Property (The value of an object’s Property)

* const a={* button:antd. button *} */
Copy the code

After processing the AST node, delete the original import import, because we have the old path of import in pluginState. PathsToRemove, best to delete the timing is ProgramExit, Remove using path.remove().


ProgramExit(path, state) {
    this.getPluginState(state).pathsToRemove.forEach(p= >! p.removed && p.remove()); }Copy the code

Congratulations to those of you who have persevered in seeing this, this is the last step, registering all AST node types we are working with with the observer

export default function({ types }) {
  let plugins = null;
  function applyInstance(method, args, context) {... }const Program = { ... }
                   
  // Add an array of AST types to register
  const methods = [ 
    'ImportDeclaration'
    'CallExpression'.'MemberExpression'.'Property'.'VariableDeclarator'.'ArrayExpression'.'LogicalExpression'.'ConditionalExpression'.'IfStatement'.'ExpressionStatement'.'ReturnStatement'.'ExportDefaultDeclaration'.'BinaryExpression'.'NewExpression'.'ClassDeclaration',]const ret = {
    visitor: { Program }, 
  };
  
  for (const method ofmethods) { ... }}Copy the code

Now that the babel-plugin-import process has been thoroughly analyzed, the reader can go through the whole process of handling on-demand loading, but without the details, the main logic is relatively straightforward.

thinking

After reading the source code and unit test, the author found that the plug-in did not convert the Switch node, so he proposed PR to the official warehouse, and now it has been integrated into the Master branch. Readers are welcome to express their thoughts in the comments section. The author mainly added SwitchStatement and processed SwitchCase with two AST nodes. SwitchStatement

SwitchStatement(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['discriminant'], path, state);
}
Copy the code

SwitchCase

SwitchCase(path, state) {
    const { node } = path;
    this.buildExpressionHandler(node, ['test'], path, state);
}
Copy the code

conclusion

This is the first time the author wrote the source code analysis of the article, but also because the author’s ability is limited, if some logic is not clear, or in the process of interpretation there are mistakes, welcome readers in the comments area to give suggestions or correction. Babel now has some apis to simplify the code or logic of babel-plugin-import, such as path.replacewithmultiple, but some of the logic that seems redundant in the source code must have a corresponding scenario, so it is retained. This plug-in has stood the test of time and is a good example for readers who need to develop babel-Plugin. Not only that, for the marginalization of functions and operating system compatibility and other details have done perfect processing. If you only need to use babel-plugin-import, this article shows some apis that are not exposed in the babel-plugin-import document and can help users of the plug-in to implement more extended functions. Therefore, I publish this article in the hope that it will help students.

The author information