Intro to transpilation using Babel
In today's post, I'll briefly explore the topic of Javascript transpilation. Many people might be familiar with the word "compilation" which in the world of software it refers to the process of transforming your higher-level code into machine code which is what computers can understand.
But what about "transpilation"? They sound similar. Are they the same thing? Not quite, otherwise, the term "transpilation" might be deemed redundant. The difference between the two lies in "the level of abstraction". Let's see an example to get what I mean.
Compilation ( C → machine code and C→assembly)
main.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int x = 2;
int y = 23;
int sum = add(x, y);
printf("%d + %d = %d", x, y, sum);
return 0;
}
Compile to machine code.
gcc main.c // produce a.out (binary code)
gcc -S -o main.s main.o // product main.s (assembly code)
Let's print out a section from complied code.
a.out
main.s
As you can see, the result of a.out
is indecipherable, and to understand main.s
requires deep knowledge of computer system assembly code. The point is that both a.out
and main.s
are at a lower layer of abstraction than main.c
; they are closer to the machine.
Transpilation (ES6 → ES5)
In contrast to compilation, which transforms your source code into something that is at a lower lever, transpilation, on the other hand, keeps the layer of abstraction approximately the same. It is also referred to as "source-to-source compilation". For example, converting a program from python2 to python3 or ES6 to ES5, notice both the source code and output code pretty stay at the same level of abstraction.
As we are focusing on JavaScript here, let's see an example of transpilation using Babel.
npm init -y
mkdir src
touch src/person.js
touch src/index.js
Let's use ES6 class
use in person.js
. Notice the use of import
and export
syntax from the ES6 modules.
src/person.js
class Person {
constructor(name) {
this.name = name
}
hello() {
return `Hello from ${this.name}`
}
}
export default Person
src/index.js
import Person from './person'
const p = new Person('Ethan')
console.log(p.hello())
Approach 1 : Using directly babel/core in a script.
- First, we install the dependencies.
npm i -D @babel/core @babel/preset-env
@babel/core
is the core module that acts as a wrapper wraps everything in Babel transformation API. Think of it as a tool that provides an entry point to your transformation pipeline.
@babel/core
itself does not know how to transform your code. This is where "plugins" come in handy. Babel plugins (or "presets", which is a group of plugins) are the ones that actually do the code transformation. Here I'll be using @babel/preset-env
, enabling us to use the latest JavaScript features.
To use @babel/core
, we first have to set up a local configuration file.
// ./babel.config.json
{
"presets": ["@babel/preset-env"]
}
Here is a short script to use Babel to transform every file in the src
directory and output transformed code to the dist
directory.
// ./babel-example.js
const path = require('path')
const fs = require('fs')
const babel = require('@babel/core')
const srcPath = path.resolve(__dirname, './src')
const distPath = path.resolve(__dirname, './dist')
if (!fs.existsSync(distPath)) {
fs.mkdirSync(distPath)
}
fs.readdir(srcPath, function (err, files) {
files.forEach(function (fileName) {
const srcFilePath = path.resolve(srcPath, `./${fileName}`)
const distFilePath = path.resolve(distPath, `./${fileName}`)
let code = babel.transformFileSync(srcFilePath, {}).code
fs.writeFileSync(distFilePath, code, { flag: 'w+' })
})
})
Run node babel_example.js
to execute to script.
Let's have a peek into the transformed dist/perform.js
file.
'use strict'
Object.defineProperty(exports, '__esModule', {
value: true,
})
exports['default'] = void 0
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
if (staticProps) _defineProperties(Constructor, staticProps)
return Constructor
}
var Person = /*#__PURE__*/ (function () {
function Person(name) {
_classCallCheck(this, Person)
this.name = name
}
_createClass(Person, [
{
key: 'hello',
value: function hello() {
return 'Hello from '.concat(this.name)
},
},
])
return Person
})()
var _default = Person
exports['default'] = _default
Method 2 : Using babel-cli
Writing a script to transform JS code is doable for a trivial example like this, but as you can imagine, it will get pretty complicated very quickly as your project grows.
Fortunately, Babel does come with a CLI package that provides us with a much easier interface to work with.
npm i -D @babel/cli
package.json
"scripts": {
"build": "babel src -d dist"
}
Simply run npm run build
. The result produced is exactly the same as in the previous method but is much easier and less error-prone.
That's it for today's post. Bye for now.
References
https://stackoverflow.com/questions/44931479/compiling-vs-transpiling
https://en.wikipedia.org/wiki/Source-to-source_compiler
https://nodejs.org/docs/latest-v13.x/api/esm.html#esm_enabling