Graduate Program KB


Babel


Overview

  1. What is Babel?
  2. Setting up babel
  3. Creating a custom plugin
  4. Hands-on

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?

  1. 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"
                    }
                }
            }]
        }
    }
    
  1. 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

  1. 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 or babel.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"]
      }
      

Target browsers for preset-env

  • Can explicitly describe the target browser environments you want to support
  • First format provides a browserslist query
  • 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
            }
        }
    }
    

Creating a custom plugin

  • Task:

    • Replace all console.log with a custom logger function
      console.log('Hello, World!');
      
      myLogger('Hello, World!');
      
  • 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

      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