Monday, March 16, 2020

What The Hell Is Babel?

INTRODUCTION

This article is simply an expansion on the excellent article posted here

https://medium.com/@bluepnume/jsx-is-a-stellar-invention-even-with-react-out-of-the-picture-c597187134b7

The reason I created this article is because I wanted to actually try what this article shows but it took me a few minutes to get that code setup and working (and the posted code actually breaks) so I figured this might save some other folks some time, plus I wanted to have a reference so I could repeat this process in the future.

JSX is a templating language which is typically used by a transpiler called 'Babel' to produce React.js code. JSX syntax is very much like Javascript syntax with inline styles though even the styles can be further broken out into separate CSS type files (which is what you tend to do when you use 'React Styled Components'). This article just concentrates on Babel (with some minor 'node' thrown in for ease of use) but you should be aware that in most cases folks also use something called webpack to produce their final product. webpack automates what we manually do near the end of this article when we manually copy files and paste them into our html file to create a browser renderable file. Also note this example uses babel-cli which means Babel from the command line, though you could also simply include Babel as a '<script>' tag in your html file and that would cause transpilation to occur at run time versus at build time. I specifically use babel-cli because I want to see what Babel actually produces and how it converts ES6 to plain vanilla ES4. I also override the React output function and provide a vanilla Javascript output function to aid in this process. 

If you are new to all of this you might struggle a bit with this post. Basically, ES4 is old style Javascript and ES6 is a newer version of Javascript and ES6 is not backwards compatible with ES4. To fix this Babel comes into the picture. You can think of Babel as a tool to convert new Javascript (ES6) to old style Javascript (ES4) although it is capable of doing far more than this. From a high level this solves most browser compatibility issues, though as I say, this is just one benefit of using Babel

What this article (and the original one it is based on) is trying to show is that even though Babel appears to be tightly coupled to React.js, it isn't. JSX is simply a templating language which can be transpiled by Babel into nearly anything. The fact that Babel is almost exclusively used to transpile JSX to React.js or Typescript is simply a reflection of how it is used. It does not have to produce either of these two target languages and so in this article I (as the original author also did) will show you exactly what that means by simply using Babel to transpile ES6 to ES4. Worth understanding if you ever have a bug you suspect was due to the conversion process. BTW you should read the original article first and if it makes sense to you and you can get it running from that article, then you really don't need to read any further. Its just that it took me a while to actually be able to put up a simple web page that used Babel to transpile ES6 to working Javascript following that example so I felt I needed a working example for future reference. Note the only reason that the original article doesn't work is it uses 'null' in two places. If you simply replace these with {id:"someId"} the example will build fine. You would still need to integrate it with your html file to get it to run in your browser and we will do this later in this article.

THE SETUP


So first, we need to have node.js installed and working on our computer. Open up a shell/command line terminal on your computer and type 

node --version

If this doesn't show you a version then install node by following the instructions shown here for Windows 

https://www.guru99.com/download-install-node-js.html

or here for Mac

https://www.webucator.com/how-to/how-install-nodejs-on-mac.cfm

Assuming all worked as expected (IE 'node --version' actually shows a version) we will now create our basic project. To do this create a directory called 'jsx_pragma'. I am using a Mac and I created this directory on my desktop so you will need to adjust accordingly. Using your GUI you could just right click on your desktop and create a new folder.

Once you have created your 'jsx_pragma' directory (some folks call it a folder) open a terminal and navigate into it ('cd ~Desktop/jsx_pragma' should work) and then create a file called package.json. Paste this into it.

{
  "name": "jsx_pragma",
  "version": "1.0.0",
  "description": "test jsx pragma",
  "main": "RenderDom.js",
  "dependencies": {
    "babel-cli": "^6.26.0"
  },
  "devDependencies": {
    "babel-preset-env": "^1.7.0"
  },
  "scripts": {
    "build": "babel src -d dist"
  },
  "author": "",
  "license": "ISC"
}

Now simply type this on the command line 

npm install

This will download all the stuff you need to actually build this project. It will figure all this out from the dependencies we put in our 'package.json' file. This is because 'npm' stands for node package manager which is part of node and this is what npm does for a living. Specifically you should see a directory called 'node_modules' and a file named 'package-lock.json' in this directory alongside the 'package.json' file we just created. 

We are not going to go too much in detail about what our 'package.json' file contains except for one key section. If you look in the 'package.json' file we created you will see this 

  "scripts": {
    "build": "babel src -d dist"
  },

Which basically defines a script we will ask npm to execute later. In our case, this script is called 'build'. It also tells npm what to run when we say 'npm run build' which in our case is babel. Additionally we are telling babel to look for input files in the directory 'src' and to place output files in the directory named 'dist'. This is pretty standard stuff but first we should actually create those two directories. You can do this however you want, just make sure these two directories exist in our 'jsx_pragma' directory. For example, you could use your GUI (right click in the 'jsx_pragma' folder and create a new folder) or you could execute these commands from the command line in the 'jsx_pragma' folder

mkdir src
mkdir dist

Once you have done this your 'jsx_pragma' folder should look something like this 

/dist
/src
/node_modules
package-lock.json
package.json

Where dist, src and node_modules are directories and package-lock.json and package.json are files. 

We are almost ready. We simply need to create two source files which are actually our project and we will be done. Create a file named 'RenderDom.js' and put it in the 'src' directory 

RenderDom.js
-------------
let renderDom = (name, props, ...children) => {
  let el = document.createElement(name);
  for (let [key, val] of Object.entries(props)){
    el.setAttribute(key, val);
  }
  for (let child of children){
    if (typeof child === 'string'){
      el.appendChild(document.createTextNode(child));
    } else {
      el.appendChild(child);
    }
  }
  return el;
}

Next create a file named 'test_render.js' and also put it in the 'src' directory 

test_render.js
--------------
/* @jsx renderDom */

function renderLogin(){
  return renderDom("section", {id:"secId"}, 
    renderDom("input", {type:"email", value:""}),
    renderDom("input", {type:"password", value:""}),
    renderDom("button", {id:"butId"}, "Log In")
  );
}

Note those are not really JSX but rather standard Javascript (thanks to Scott for pointing that out) but since we are really interested in Babel we won't worry about that. Now we can run the following command and we should get two files in our 'dist' folder.

npm run build

We are almost done. The reason I say almost is for some reason the babel presets don't get set automatically. I have no idea why. I mean they are in our 'package.json' file but still, they don't work. To fix this you could alter the babel command line in the 'package.json' file to include them but I prefer to simply create a .babelrc file and put them in there. So, create a new file in the 'jsx_pragma' directory and call it '.babelrc' and stick this in there (NOTE don't forget the leading dot when you create .babelrc !)

{
  "presets" : [ "/Users/kensmith/Desktop/jsx_pragma/node_modules/babel-preset-env" ]
}

Two things to note. First you will need to change this line to point to where YOUR 'jsx_pragma' folder is located. In the example above it is located at '/Users/kensmith/Desktop/' on my machine. I am assuming yours is not in a directory called '/Users/kensmith'. Also, you must use an absolute path. Do not try to take a shortcut and use a relative path. Specifically, this works 

/Users/kensmith/Desktop/jsx_pragma/node_modules/babel-preset-env

This does not work

jsx_pragma/node_modules/babel-preset-env

I believe this is a babel6 limitation. 

Once you have created your .babelrc file and placed it in the proper directory and modified the absolute path in that file to point to YOUR 'jsx_pragma' directory location you should be able to run 

npm run build

Remember, our output will be located in our 'dist' directory and we will need to put it in a html file to actually see it in action in our browser. You could run it just using node but that's no fun, so create the following file and put it on your desktop (or somewhere you can easily find) and call it test.html. 

test.html
--------
<html>
  <head>
  </head>
  <body>
    <div id="ruut" name="ruut">
    </div>
    <script>
var wtf = renderLogin();
document.getElementById("ruut").appendChild(wtf);
    </script>
  </body>
<html>

All we are doing here is creating a simple html file with a div called 'ruut' and two lines of Javascript. The first will call the 'renderLogin()' function we created in our 'test_render.js' file and assign its output to a variable named 'wtf'. The second simply appends this output to our div named 'ruut' using the standard DOM appendChild() method. 

Finally, in the 'dist' directory will be two files. Open each and copy the contents of them into the html file right after the '<script>' tag (normally something called webpack does this as part of the build process but here we will do it manually). When you are done your html file should now look something like this 

test.html
--------
<html>
  <head>
  </head>
  <body>
    <div id="ruut">
    </div>
    <script>

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var renderDom = function renderDom(name, props) {
  for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
    children[_key - 2] = arguments[_key];
  }

  var el = document.createElement(name);
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = Object.entries(props)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var _ref = _step.value;

      var _ref2 = _slicedToArray(_ref, 2);

      var key = _ref2[0];
      var val = _ref2[1];

      el.setAttribute(key, val);
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  var _iteratorNormalCompletion2 = true;
  var _didIteratorError2 = false;
  var _iteratorError2 = undefined;

  try {
    for (var _iterator2 = children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
      var child = _step2.value;

      if (typeof child === 'string') {
        el.appendChild(document.createTextNode(child));
      } else {
        el.appendChild(child);
      }
    }
  } catch (err) {
    _didIteratorError2 = true;
    _iteratorError2 = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion2 && _iterator2.return) {
        _iterator2.return();
      }
    } finally {
      if (_didIteratorError2) {
        throw _iteratorError2;
      }
    }
  }

  return el;
};

/* @jsx renderDom */

function renderLogin() {
  return renderDom("section", {id:"secId"}, renderDom("input", { type: "email", value: "" }), renderDom("input", { type: "password", value: "" }), renderDom("button", {id:"butId"}, "Log In"));
}

var wtf = renderLogin();
document.getElementById("ruut").appendChild(wtf);
    </script>
  </body>
<html>


Simply double click this file (after you save it to your desktop) and you will see your transpiled code running in the browser. Specifically you should see two inputs and a button. Babel has transpiled our ES6 input to plain Javascript; ES4 style. 


WHAT WE DID


So first we created a project for npm to build. This project uses Babel to take any files in the 'src' directory and transpile them and put the transpiled output in the 'dist' directory. This transpiling simply converted a JSX template file (which in our case is really just ES6 code) to an ES4 plain vanilla Javascript file. We did this to two files, 'RenderDom.js' and 'test_render.js'.

If you look at our 'test_render.js' file we see it does not contain very much. Specifically it contains 

/* @jsx renderDom */

function renderLogin(){
  return renderDom("section", null,
    renderDom("input", {type:"email", value:""}),
    renderDom("input", {type:"password", value:""}),
    renderDom("button", null, "Log In")
  );
}

The first line is NOT just a comment. It is known as a 'pragma comment' or 'jsx pragma' or 'custom pragma'. I have seen it called all this and more but these are the most common terms. The technical explanation for this line may be found here

https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#custom

Basically, it is telling Babel what function to use to transform (or transpile) your JSX. In our case we are telling it to the use the function 'renderDom()' which we created in our other file (RenderDom.js).

The rest of this file is vanilla Javascript. In our example, we are using a function called 'renderDom()' instead of the standard React function called 'React.createElement()' which is what JSX is typically used for (to create a React application) and so rather than JSX being converted to React in our example, ES6 is being converted to plain vanilla Javascript, version ES4 by Babel

We defined the function 'renderDom()' in our second file called 'RenderDom.js'. Now what is interesting and why I went through all of this exercise was to see what exactly Babel does when it transpiles a file. Looking at the files Babel output in the 'dist' directory which we copied into our html file is interesting. It didn't do much to 'test_render.js' because it was pretty much just standard ES4 Javascript to begin with, but it did do a number on our 'RenderDom.js' file. Specifically this code

let renderDom = (name, props, ...children) => {
  let el = document.createElement(name);
  for (let [key, val] of Object.entries(props)){
    el.setAttribute(key, val);
  }
  for (let child of children){
    if (typeof child === 'string'){
      el.appendChild(document.createTextNode(child));
    } else {
      el.appendChild(child);
    }
  }
  return el;
}

Was converted to this code 

'use strict';

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var renderDom = function renderDom(name, props) {
  for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
    children[_key - 2] = arguments[_key];
  }

  var el = document.createElement(name);
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = Object.entries(props)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var _ref = _step.value;

      var _ref2 = _slicedToArray(_ref, 2);

      var key = _ref2[0];
      var val = _ref2[1];

      el.setAttribute(key, val);
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  var _iteratorNormalCompletion2 = true;
  var _didIteratorError2 = false;
  var _iteratorError2 = undefined;

  try {
    for (var _iterator2 = children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
      var child = _step2.value;

      if (typeof child === 'string') {
        el.appendChild(document.createTextNode(child));
      } else {
        el.appendChild(child);
      }
    }
  } catch (err) {
    _didIteratorError2 = true;
    _iteratorError2 = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion2 && _iterator2.return) {
        _iterator2.return();
      }
    } finally {
      if (_didIteratorError2) {
        throw _iteratorError2;
      }
    }
  }

  return el;
};

Wow! That's a lot of code. At a minimum, if we are getting paid for lines of code Babel is our friend :-)

In effect this is the difference between ES4 and ES6. ES6 can be written more concisely than ES4 and it also added some exception handling in there for us for free.

Now let's see what happens when we refactor that method ourselves to use ES4 Javascript. You would think Babel would have nothing to do. Here is our new renderDom() method in old school ES4


// Note IE8 needs polyfill for Object.entries and Object.keys

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
Object.keys = (function() {
  'use strict';
  var hasOwnProperty = Object.prototype.hasOwnProperty,
      hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
      dontEnums = [
        'toString',
        'toLocaleString',
        'valueOf',
        'hasOwnProperty',
        'isPrototypeOf',
        'propertyIsEnumerable',
        'constructor'
      ],
      dontEnumsLength = dontEnums.length;

  return function(obj) {
    if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
      throw new TypeError('Object.keys called on non-object');
    }

    var result = [], prop, i;

    for (prop in obj) {
      if (hasOwnProperty.call(obj, prop)) {
        result.push(prop);
      }
    }

    if (hasDontEnumBug) {
      for (i = 0; i < dontEnumsLength; i++) {
        if (hasOwnProperty.call(obj, dontEnums[i])) {
          result.push(dontEnums[i]);
        }
      }
    }
    return result;
  };
}());

Object.entries = function( obj ){
  var ownProps = Object.keys( obj ),
      i = ownProps.length,
      resArray = new Array(i); // preallocate the Array
  while (i--)
    resArray[i] = [ownProps[i], obj[ownProps[i]]];
  
  return resArray;
};


function renderDom(name, props, ...children) {
  let el = document.createElement(name);
  for (let [key, val] of Object.entries(props)){
    el.setAttribute(key, val);
  }
  for (let child of children){
    if (typeof child === 'string'){
      el.appendChild(document.createTextNode(child));
    } else {
      el.appendChild(child);
    }
  }
  return el;
}


Well Babel might have nothing to do but we certainly had a lot to do.

Now you might be wondering what happened to our simple renderDom() method. Well first, if we are worried about backward compatibility with IE8 (and isn't everybody), then even ES4 has issues, specifically, IE8 (or older) does not support Object.entries() nor Object.keys(). As a result, we have to create a polyfill for both methods just to allow our renderDom() method to compile. Note that we could have also chose to refactor renderDom() to not use Object.entries() but it would have gotten ugly in a hurry and that's kind of the point. This is exactly what Babel is is good at. Specifically it will handle all of this for us so we can concentrate on writing good clean Javascript without having to worry about ES4 versus ES6 and browser incompatibilities. Babel handles all that for us, and more.

Here is what Babel transpiled our converted renderDom.js to 

'use strict';

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

// IE8 needs polyfill for Object.entries and Object.keys

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
Object.keys = function () {
  'use strict';

  var hasOwnProperty = Object.prototype.hasOwnProperty,
      hasDontEnumBug = !{ toString: null }.propertyIsEnumerable('toString'),
      dontEnums = ['toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor'],
      dontEnumsLength = dontEnums.length;

  return function (obj) {
    if (typeof obj !== 'function' && ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object' || obj === null)) {
      throw new TypeError('Object.keys called on non-object');
    }

    var result = [],
        prop,
        i;

    for (prop in obj) {
      if (hasOwnProperty.call(obj, prop)) {
        result.push(prop);
      }
    }

    if (hasDontEnumBug) {
      for (i = 0; i < dontEnumsLength; i++) {
        if (hasOwnProperty.call(obj, dontEnums[i])) {
          result.push(dontEnums[i]);
        }
      }
    }
    return result;
  };
}();

Object.entries = function (obj) {
  var ownProps = Object.keys(obj),
      i = ownProps.length,
      resArray = new Array(i); // preallocate the Array
  while (i--) {
    resArray[i] = [ownProps[i], obj[ownProps[i]]];
  }return resArray;
};

function renderDom(name, props) {
  var el = document.createElement(name);
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = Object.entries(props)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var _ref = _step.value;

      var _ref2 = _slicedToArray(_ref, 2);

      var key = _ref2[0];
      var val = _ref2[1];

      el.setAttribute(key, val);
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
    children[_key - 2] = arguments[_key];
  }

  var _iteratorNormalCompletion2 = true;
  var _didIteratorError2 = false;
  var _iteratorError2 = undefined;

  try {
    for (var _iterator2 = children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
      var child = _step2.value;

      if (typeof child === 'string') {
        el.appendChild(document.createTextNode(child));
      } else {
        el.appendChild(child);
      }
    }
  } catch (err) {
    _didIteratorError2 = true;
    _iteratorError2 = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion2 && _iterator2.return) {
        _iterator2.return();
      }
    } finally {
      if (_didIteratorError2) {
        throw _iteratorError2;
      }
    }
  }

  return el;
}

That's a lot of code. Looking at the original renderDom.js file is a lot easier on the eyes and probably easier to debug and maintain. It is good, however, to see what exactly Babel does to your code. If you're like me and distrustful of other code wonking your code you can use this approach to actually see how your code was transformed (or transpiled) by Babel. Who knows, maybe it produces a bug under certain circumstances. It wouldn't be the first time a compiler (or in this case transpiler) bit me in the ass. I don't know about you, but at least I'll now be able to sleep better at night.






No comments: