Babel: Everything You Need to Know
As you read through this article, you may already be a skilled front-end developer. But do you truly understand the tech stack behind your project? What do terms like Babel and webpack mean to you? Why can React projects written in JSX or TSX be transformed into JavaScript files that Node.js can execute?
If these questions still puzzle you, congratulations! This article will guide you step by step to uncover the mystery of the compilation process. While it won't delve into every detail, it will provide you with a comprehensive, high-level perspective.
Babel was originally designed with a clear purpose: to seamlessly transform modern JavaScript code into older versions to ensure compatibility.
This need arose from the diversity of browsers, including Chrome, Safari, Firefox, and others. Each of these browsers, across their different brands and versions, has its own JavaScript execution environment. This means that modern JavaScript code might not run on all browsers.
However, as developers, we are always eager to adopt the latest JavaScript features to improve development efficiency. For instance, ES6 introduced the class
keyword, making class definitions more intuitive and concise. Compared to the traditional prototype-based inheritance, this was a significant improvement.
// ES5
function Student(name) {
this.name = name;
}
// ES6
class Student {
constructor(name) {
this.name = name;
}
}
In ES6, class
is merely a syntactic improvement. Its underlying execution mechanism is no different from traditional functions. However, for developers, this improvement greatly enhances the ease of coding and the readability of the code.
Now that we’ve understood the background of Babel, let’s dive into how Babel works. Below, we’ll walk through its working principles step by step.
Babel is a JavaScript compiler that allows developers to write code using the latest JavaScript syntax, then transforms it into code that can run in current and older browsers or other environments. The working process of Babel can be broken down into three main stages: Parsing, Transformation, and Generation.
The Babel workflow begins with the Parsing stage. In this stage, Babel takes the source code as input and uses a parser to convert it into an Abstract Syntax Tree (AST). AST is a tree-like structure that represents the syntactic structure of the code based on the programming language’s grammar.
For example, when Babel parses the following ES6 code:
const square = (x) => x * x;
It generates a corresponding AST. This AST describes various syntax elements in the code, such as variable declarations, arrow functions, parameters, and expressions.
If you’re curious about ASTs, you can visit this website for real-time debugging: AST Explorer.
Once the source code has been parsed into an AST, Babel moves into the Transformation stage. During this stage, Babel traverses the AST and applies a series of plugins or presets to modify its structure. These plugins and presets contain rules that transform new syntax into older syntax.
In fact, a preset is simply a collection of plugins.
Continuing from the previous example, Babel would use a plugin to identify the arrow function and transform it into a traditional function expression, ensuring compatibility with older browsers that don’t support arrow functions.
After the transformation stage, the AST has been modified to conform to the target environment’s JavaScript syntax standards.
Finally, Babel enters the Generation stage. In this stage, Babel uses a generator to convert the transformed AST back into a source code string. This process is called code generation. The generated code uses JavaScript syntax compatible with the target environment.
For the previous example, the generator would transform the modified AST into the following ES5-compatible code:
var square = function square(x) {
return x * x;
};
This final code can be executed in any JavaScript environment that supports ES5.
No need for too many words—let’s jump straight into the code.
First, create a new directory, add a src
folder, and create a file named index.ts
inside it. Then, write the following code in index.ts
:
// A utility class to calculate the number of days between two dates
class DateCalculator {
constructor() {
this.days = 0;
}
days: number;
date1: Date;
date2: Date;
addDays(date: Date, days: number) {
date.setDate(date.getDate() + days);
return date;
}
calcDays() {
this.days =
Math.abs(this.date1.getTime() - this.date2.getTime()) /
(1000 * 60 * 60 * 24);
}
printDays() {
console.log(this.days);
}
}
const date1 = new Date(2020, 1, 1);
const date2 = new Date(2020, 1, 10);
const dateCalculator = new DateCalculator();
dateCalculator.date1 = date1;
dateCalculator.date2 = date2;
dateCalculator.calcDays();
dateCalculator.printDays();
This is a utility class for date calculations. Now, let’s try running it with Node.js:
node src/index.ts
Did it throw an error? That’s expected because Node.js doesn’t natively support .ts
files. Next, we’ll use Babel to compile it into JavaScript.
Run the following command in your terminal:
pnpm add @babel/cli @babel/core @babel/preset-typescript -D
Before using Babel, let’s briefly understand its components.
Babel’s core functionality is implemented through plugins. Each plugin handles a specific transformation task. For convenience, Babel provides presets, which are collections of plugins designed to handle common transformations.
In our project, we’ll use the @babel/preset-typescript
preset to handle TypeScript code. This preset parses the AST (Abstract Syntax Tree) of TypeScript code, removes the type annotations, and compiles it down to JavaScript.
Next, create a babel.config.json
file at the root of your project and add the following configuration:
{
"presets": ["@babel/preset-typescript"]
}
Now, we can use Babel to compile our TypeScript code. Run the following command:
npx babel src --out-dir dist --extensions ".ts"
This command compiles all TypeScript files in the src
directory into JavaScript files and outputs them into the dist
directory.
Note: Unlike TypeScript's
tsc
command, Babel does not perform type checking before converting TypeScript to JavaScript.
Babel only parses the AST and synthesizes it back into JavaScript—nothing more.
Finally, try running the compiled code:
node dist/index.js
This time, you should see the correct output, showing the difference in days between the two dates.
Run the following commands to install react
, react-dom
, and their type definitions (@types/react
and @types/react-dom
):
pnpm add react react-dom
pnpm add @types/react @types/react-dom -D
Once installed, you can start writing a simple TSX component. Create a new file App.tsx
in the src
directory and add the following code:
import React from "react";
export function App() {
return (
<div>
<h1>Hello, TSX!</h1>
</div>
);
}
Next, install the @babel/preset-react
preset. Without this preset, Babel won’t transform <div></div>
into React.createElement('div')
. While it may still compile successfully, it won’t run properly in Node.js.
pnpm add @babel/preset-react -D
Now, update your Babel configuration file (babel.config.json
) to include the @babel/preset-react
preset:
{
"presets": ["@babel/preset-typescript", "@babel/preset-react"]
}
Compile your TSX code with Babel using the following command:
npx babel src --out-dir dist --extensions ".tsx,.ts"
After compilation, you’ll find the transformed JavaScript files in the dist
directory. If you try executing dist/index.js
, it will run successfully. However, running dist/App.js
will fail because of Node.js's handling of ES Modules. To fix this, we need to install @babel/preset-env
.
@babel/preset-env
is an intelligent preset that allows you to use the latest JavaScript features without worrying about cross-environment compatibility. It manages syntax conversions and polyfills automatically, ensuring a smaller and more efficient JavaScript bundle.
Install the preset using:
pnpm add @babel/preset-env -D
Update your Babel configuration file to include @babel/preset-env
:
{
"presets": [
"@babel/preset-typescript",
"@babel/preset-react",
"@babel/preset-env"
]
}
With this setup, you should now be able to run all the compiled JavaScript files without issues.
Are you suddenly brimming with confidence in Babel? You might feel ambitious now, thinking you could start coding with just Babel, no need for webpack.
Let’s get started! First, replace the existing content in your file with the following code:
// src/index.ts --> src/index.tsx
import { App } from "./App.tsx";
import { renderToString } from "react-dom/server";
import React from "react";
const domString = renderToString(<App />);
console.log(domString);
Now, run the following command in your terminal:
npx babel src --out-dir dist --extensions ".tsx"
After the transformation, try running:
node dist/index.js
Did it fail again? You might notice an error indicating that the App object exported from App.tsx
cannot be found. Why is that? Let’s take a look at the actual code in dist/index.js
:
"use strict";
var _App = require("./App.tsx");
var _server = require("react-dom/server");
var _react = _interopRequireDefault(require("react"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
var domString = (0, _server.renderToString)(/*#__PURE__*/_react["default"].createElement(_App.App, null));
console.log(domString);
You’ll see that the compiled code is still referencing ./App.tsx
, while our compiled output is App.js
. Consequently, it can't find the App object.
What should we do? We need to implement a bundler that ensures stable references, whether the imported files have been transformed or not. Before bundling, the references should point to the original files; after bundling, they should point to the bundled files. Additionally, we need to bundle CSS, static resources, and ideally consolidate all JS code into a single file.
Good news! This is where webpack comes into play.
Webpack is a powerful tool in front-end development that is responsible for bundling various resources in a project (such as JavaScript, CSS, images, etc.) into files that can be efficiently loaded by the browser.
It starts from a specified entry file, recursively finding all dependencies and using different loaders to process the various resources. For example:
babel-loader
is used to transform JavaScript code.style-loader
andcss-loader
are used to handle CSS files.
By doing this, Webpack can bundle multiple modules and resources into one or more files, optimizing the loading performance of the project.
In fact, I previously wrote an introductory article about Webpack. If you're interested, feel free to check it out here.