DEV Community

pulkitnagpal
pulkitnagpal

Posted on

Transpile JSX using your own custom built babel plugin

Ever wondered how does react jsx code (<div>Hello World</div>) gets compiled to React.createElement("div", null, "Hello World"). This blog is all about this compilation process by taking help from the source code of babel-preset-react and trying to build our own custom plugin.

Just to make things clear, I will not use Webpack at all because its significance lies in just bundling process. It has nothing to do with transpilation part. I will just use babel and 3 files. Thats it. No HTML nothing. The goal of this blog is to actually convert this jsx code to js code that browsers can really understand.

Github Link -> https://github.com/pulkitnagpal/custom-jsx-plugin

Before moving straight to the code, lets revise some basics

Basics

I tried this <div>Hello world</div> code in a normal script tag and got "Unexpected token <". I thought create-react-app does everything under the hood and does some magic to convert it into React.createElement syntax.
Everyone must know that this div in jsx is not an actual HTML Element. The compilation process just converts it into a function call. Not into HTML Element. That part is done by react.

I dug further and gradually realised that there is some power(apologies for using it :P) that converts this jsx into a function call like syntax. This power is harnessed by BABEL.

create-react-app and many other tools uses babel under the hood.

How does Babel works ?

  1. Parses your code => Babel converts your code to AST(Abstract Syntax Tree). Heavy term right ? No problem try this tool (https://astexplorer.net/). Try writing something on the left and a tree like structure will be generated on the right. This is done by a parser built inside babel.
  2. Traverse & Transform => This is where babel plugins and presets comes into play. A visitor pattern is provided by babel that let us traverse through all the tree nodes of AST and transform/manipulate those nodes into something we desire.
  3. Generate => This is the stage where babel converts the transformed tree back into human readable code.

Before moving to our own custom plugin, let's try to use already built react preset and transpile our index file using babel cli.

  1. Steps to install babel-cli are mentioned here
  2. Install React and ReactDOM and react preset
  3. Create an index.js file and .babelrc file

Add this to index file

ReactDOM.render(<div><p>Hello World</p></div>, document.getElementById("root"))

and this to .babelrc

{
  "presets": ["react"]
}

Run this command on the terminal
node ./node_modules/babel-cli/bin/babel index.js

and we can see the transpiled code on the terminal screen. We can also create separate output file. But I wanted to make things simple. As we can see how this jsx code got transpiled to React createElement syntax. We will try to build our own plugin that will do the same thing.

NOTE: I will ignore the props and attributes part of the jsx in the custom plugin.

Custom-jsx-plugin

Clear the .babelrc file.

Create a new file custom-jsx-plugin.js

Try below code in (https://astexplorer.net/) to get an overview on how jsx code looks like in AST

function anything() {
  return <div><p>Hello World</p></div>
}

and as we can see on the right side. The jsx part has a node type of JSXElement. This is what we need to manipulate and replace it with a CallExpression as React.createElement is actually a javascript function.

When you try to parse this jsx using your local babel cli, you will get a syntax error. Because the parser does not know anything about the jsx syntax.
Thats why we need to add a file which manipulates the parser, name it as jsx-syntax-parser.js

jsx-syntax-parser.js

module.exports = function () {
  return {
    manipulateOptions: function manipulateOptions(opts, parserOpts) {
      parserOpts.plugins.push("jsx");
    }
  };
};

and now our new .babelrc file will look like

{
  "plugins": ["./custom-jsx-plugin", "./jsx-syntax-parser"]
}

The order of plugins matter and its actually in the reverse order. Right to Left. First our syntax parser will be executed which tells babel that it needs to parse jsx syntax also and then it will execute our custom plugin file which is for now empty.

As we still have not written anything inside our custom-jsx-plugin file. The output of babel transpilation will be same as the index file. Nothing should have been changed.

Add this to custom-jsx-plugin file

module.exports = function (babel) {
  var t = babel.types;
  return {
    name: "custom-jsx-plugin",
    visitor: {
      JSXElement(path) {
        //get the opening element from jsxElement node
        var openingElement = path.node.openingElement;  
         //tagname is name of tag like div, p etc
        var tagName = openingElement.name.name;
        // arguments for React.createElement function
        var args = []; 
        //adds "div" or any tag as a string as one of the argument
        args.push(t.stringLiteral(tagName)); 
        // as we are considering props as null for now
        var attribs = t.nullLiteral(); 
        //push props or other attributes which is null for now
        args.push(attribs); 
        // order in AST Top to bottom -> (CallExpression => MemberExpression => Identifiers)
        // below are the steps to create a callExpression
        var reactIdentifier = t.identifier("React"); //object
        var createElementIdentifier = t.identifier("createElement"); //property of object
        var callee = t.memberExpression(reactIdentifier, createElementIdentifier)
        var callExpression = t.callExpression(callee, args);
         //now add children as a third argument
        callExpression.arguments = callExpression.arguments.concat(path.node.children);
        // replace jsxElement node with the call expression node made above
        path.replaceWith(callExpression, path.node); 
      },
    },
  };
};

And thats it. These 12 lines of code can easily transpile our jsx code.
Run this command again on the terminal
node ./node_modules/babel-cli/bin/babel index.js

and notice that the result is the same as created by react-preset

like this
ReactDOM.render(React.createElement("div", null, React.createElement("p", null, Hello World)), document.getElementById("root"));

Explanation of the code

  1. In the visitor pattern of babel, during traversal of the AST, for every JSXElement node, this callback function as defined above will be executed.
  2. This node has two parts opening and closing elements. The name of opening element (eg "div") is extracted to be used as first argument of the function (React.createElement)
  3. The second argument (props or attributes) is considered as null for this example. Ignoring props just for simplicity.
  4. Now to create a function call, we will need to create 3 things CallExpression => MemberExpression => Identifiers. The 2 identifiers used here are obviously React as an object and createElement as the property.
  5. Then we need to concat the rest arguments which are the child nodes of current node.
  6. At last we need to replace(used inbuilt function of path) the current JSXElement node with the callExpression node you have created. This modifies the AST.

Conclusion

This is obviously not a production ready code. I have taken help from the source code of babel-preset-react and just to make things simpler I made the code shorter for better understanding. It is just the basic overview of how does this plugin works under the hood.

Top comments (0)