Babel
Overview
What is Babel?
- Babel is a toolchain mainly for converting ECMAScript 2015+ code into backwards compatible JavaScript
- Some features:
- Transforming syntax: Converting equivalent functional code to an older syntax
- Polyfills: Adding missing functionality
- Custom plugins: Flexibility in applying complex transformations across a code base reliably
// Babel input: ES2015 arrow function
[1, 2, 3].map(n => n + 1);
// Babel output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
Brief History
- Babel was created in September, 2014
- Its name was originally "6to5", derived from the usage of transpiling ES6 code to ES5
- It gained popularity quickly and turned into Babel, becoming a tooling platform for developers
- Still serves as a modern JS transpiler, but also has an open API for integrating with other tools
Why use Babel?
- ECMA has released yearly updates to JavaScript, therefore using Babel ensures new features of JS can be used regardless of browser support
- Without Babel, browsers aren't guaranteed to integrate new modern features or may take a long time to do so
How does it work?
- Babel parses the source code into an AST, performing lexical analysis and syntactic analysis to abstract semantic details
- Utilises the @babel/parser package and its parse functionality
- For example, consider the simple square function. Here are the important details an AST may be concerned with:
- Type of statement / node: Function
- Name: square
- Parameters: n
- Function body: An array of statements with an optional return statement
function square(n) { return n * n; }
- Here are the semantic details represented in a sub-tree of an AST:
{ "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "square" }, "params": [ { "type": "Identifier", "name": "n" } ], "body": { "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument": { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "n" }, "operator": "*", "right": { "type": "Identifier", "name": "n" } } }] } }
- Generate a new AST by applying a set of transformations to the original AST
- Utilises @babel/traverse package and its traverse functionality to visit noes and apply transformations via the visitor pattern
- Modifications to the AST are done in-place for multiple transformations
- Convert the new AST into transpiled source code
- Utilises @babel/generator package for generating new code
Setting up Babel
- Installation:
npm install --save-dev @babel/core @babel/cli @babel/preset-env
@babel/core
- Transpilation tools, parsing, applying transforms, generation@babel/cli
- CLI for using babel in terminal or scripts@babel/preset-env
preset ensuring new JS features are backwards-compatible
Running babel
babel <target> -d <output_target>
- Target input directory / file
- Target output directory
- Some flags:
--out-file
: Output all input files in a single transpiled output file--presets
: Specify presets for the command--plugins
: Specify plugins for the command--watch
: Automatically transpiles files when they change (great for testing)
Configuration files
- Project-wide:
babel.config.json
orbabel.config.js
- File-relative:
.babelrc.json
or.babelrc.js
- JavaScript is good for dynamic configurations, then you can apply different settings based off logic
Babel plugins
-
Plugins are functions which apply code transformations
-
Order matters, apply from left to right
-
Babel resolves plugins on npm by looking at the node_modules directory, otherwise specify a path to a custom plugin
-
Plugins with provided options are wrapped in another array
{ "plugins": [ "@babel/plugin-transform-block-scoping" ["@babel/plugin-transform-arrow-functions", { "spec": true }], ] }
Babel presets
-
Presets are a set of plugins, usually tailored for a specific work environment and easier than configuring multiple specific plugins
-
Apply from right to left, plugins run before presets
-
Can also provide options analogous to plugins
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"] }
-
Some commonly used presets:
- @babel/preset-env: A smart preset dynamically selecting plugins and polyfill that are needed for the target environment
- @babel/preset-typescript: Supports TypeScript
- @babel/preset-react: Supports React
-
Custom presets
- Can specify plugins, other presets and options
// src/custom-preset.js module.exports = () => ({ "presets": ["presetA"], "plugins": ["pluginA"], ... })
// babel.config.json { "presets": ["./src/custom-preset.js"] }
- Can specify plugins, other presets and options
Target browsers for preset-env
- Can explicitly describe the target browser environments you want to support
- First format provides a browserslist query
- browserslist: GitHub
- Second format is an object of minimum browser environment versions to support
- Not providing a target will cause Babel to assume you want to target the oldest browsers possible, transforming your code to be ES5 compatible
{ "targets": "> 0.25%, not dead" }
{ "targets": { "chrome": "58", "ie": "11", "firefox": "90" } }
Polyfills
- Polyfills are pieces of code used to add missing functionality, since some modern features aren't available in older versions
- Ex. polyfills and array functions
- @babel/polyfill is deprecated, use core-js to import polyfills individually, reducing bundle size
- Using polyfills with @babel/preset-env:
- Install core-js from npm
- Specify useBuiltIns and corejs in options
{ "presets": [["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 }]] }
Some other configuration options
- ignore: Files / directories to be excluded
- include: Files / directories to be included
- comments: Include comments in output (true, false)
- compact: Omit newlines and whitespace (auto, false, true)
- minified: Applies compact, shortens some statements / expressions
- env: Define other configurations for specific environments
- Ex.
NODE_ENV=prod babel src -d dist
{ "presets": ["@babel/preset-env"], "env": { "prod": { "minified": true } } }
- Ex.
Creating a custom plugin
-
Task:
- Replace all console.log with a custom logger function
console.log('Hello, World!');
myLogger('Hello, World!');
- Replace all console.log with a custom logger function
-
Export a function passing the babel object
-
The object has property types, providing utilities for creating nodes and checking node types
-
More information: https://babeljs.io/docs/babel-types
module.exports = function loggerPlugin({ types }) { ... }
-
Return an object with a visitor object
function loggerPlugin({ types }) { return { visitor: {} } }
-
Finding our target node:
-
We want to replace the console.log call with our own logger function
-
Use parse from @babel/parser or AST explorer
-
In our case, we want to target the CallExpression node
- It has properties callee and arguments
- Create a visitor function for this node
- Check out the babel plugin handbook for more information on the path object: https://github.com/kentcdodds/babel-plugin-handbook
function loggerPlugin({ types }) { return { visitor: { CallExpression(path) {} } } }
-
-
Implementation for the condition:
- Different types of callee within CallExpression
- Ex. MemberExpression, Identifier
- Only want to target console.log calls
CallExpression(path) { const { node } = path; if ( types.isMemberExpression(node.callee) && node.callee.object.name === 'console' && node.callee.property.name === 'log' ) {} }
-
Implementation for replacing the node
CallExpression(path) { const { node } = path; if (...) { const newCallee = types.identifier('customLogger'); const newCallExpression = types.callExpression( newCallee, node.arguments ); path.replaceWith(newCallExpression); } }
-
Usage: Add it in the configuration file
{ "plugins": ["./path/to/loggerPlugin"] }
Hands-on
-
Create a plugin which prefixes all function names under test
-
The user can set custom options prefix and skipPrefixed
-
Setup:
git clone https://github.com/Khai-Yiu/babel-plugin-hands-on.git
cd babel-plugin-hands-on
npm i
npm run test:watch