• Basics" level="1">Basics
    • ASTs" level="2">ASTs
    • Stages of Babel" level="2">Stages of Babel
      • Parse" level="3">Parse
        • Lexical Analysis" level="4">Lexical Analysis
        • Syntactic Analysis" level="4">Syntactic Analysis
      • Transform" level="3">Transform
      • Generate" level="3">Generate
    • Traversal" level="2">Traversal
      • Visitors" level="3">Visitors
      • Paths" level="3">Paths
        • Paths in Visitors" level="4">Paths in Visitors
      • State" level="3">State
      • Scopes" level="3">Scopes
        • Bindings" level="4">Bindings

    Basics" class="reference-link">Basics

    Babel is a JavaScript compiler, specifically a source-to-source compiler, often called a “transpiler”. This means that you give Babel some JavaScript code, Babel modifies the code, and generates the new code back out.

    ASTs" class="reference-link">ASTs

    Each of these steps involve creating or working with an Abstract Syntax Tree or AST.

    Babel uses an AST modified from ESTree, with the core spec located here.

    1. function square(n) {
    2. return n * n;
    3. }

    Check out AST Explorer to get a better sense of the AST nodes. Here is a link to it with the example code above pasted in.

    This same program can be represented as a tree like this:

    1. - FunctionDeclaration:
    2. - id:
    3. - Identifier:
    4. - name: square
    5. - params [1]
    6. - Identifier
    7. - name: n
    8. - body:
    9. - BlockStatement
    10. - body [1]
    11. - ReturnStatement
    12. - argument
    13. - BinaryExpression
    14. - operator: *
    15. - left
    16. - Identifier
    17. - name: n
    18. - right
    19. - Identifier
    20. - name: n

    Or as a JavaScript Object like this:

    1. {
    2. type: "FunctionDeclaration",
    3. id: {
    4. type: "Identifier",
    5. name: "square"
    6. },
    7. params: [{
    8. type: "Identifier",
    9. name: "n"
    10. }],
    11. body: {
    12. type: "BlockStatement",
    13. body: [{
    14. type: "ReturnStatement",
    15. argument: {
    16. type: "BinaryExpression",
    17. operator: "*",
    18. left: {
    19. type: "Identifier",
    20. name: "n"
    21. },
    22. right: {
    23. type: "Identifier",
    24. name: "n"
    25. }
    26. }
    27. }]
    28. }
    29. }

    You’ll notice that each level of the AST has a similar structure:

    1. {
    2. type: "FunctionDeclaration",
    3. id: {...},
    4. params: [...],
    5. body: {...}
    6. }
    1. {
    2. type: "Identifier",
    3. name: ...
    4. }
    1. {
    2. type: "BinaryExpression",
    3. operator: ...,
    4. left: {...},
    5. right: {...}
    6. }

    Note: Some properties have been removed for simplicity.

    Each of these are known as a Node. An AST can be made up of a single Node, or hundreds if not thousands of Nodes. Together they are able to describe the syntax of a program that can be used for static analysis.

    Every Node has this interface:

    1. interface Node {
    2. type: string;
    3. }

    The type field is a string representing the type of Node the object is (e.g. "FunctionDeclaration", "Identifier", or "BinaryExpression"). Each type of Node defines an additional set of properties that describe that particular node type.

    There are additional properties on every Node that Babel generates which describe the position of the Node in the original source code.

    1. {
    2. type: ...,
    3. start: 0,
    4. end: 38,
    5. loc: {
    6. start: {
    7. line: 1,
    8. column: 0
    9. },
    10. end: {
    11. line: 3,
    12. column: 1
    13. }
    14. },
    15. ...
    16. }

    These properties start, end, loc, appear in every single Node.

    Stages of Babel" class="reference-link">Stages of Babel

    The three primary stages of Babel are parse, transform, generate.

    Parse" class="reference-link">Parse

    The parse stage, takes code and outputs an AST. There are two phases of parsing in Babel: Lexical Analysis and Syntactic Analysis.

    Lexical Analysis" class="reference-link">Lexical Analysis

    Lexical Analysis will take a string of code and turn it into a stream of tokens.

    You can think of tokens as a flat array of language syntax pieces.

    1. n * n;
    1. [
    2. { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
    3. { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
    4. { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
    5. ...
    6. ]

    Each of the types here have a set of properties describing the token:

    1. {
    2. type: {
    3. label: 'name',
    4. keyword: undefined,
    5. beforeExpr: false,
    6. startsExpr: true,
    7. rightAssociative: false,
    8. isLoop: false,
    9. isAssign: false,
    10. prefix: false,
    11. postfix: false,
    12. binop: null,
    13. updateContext: null
    14. },
    15. ...
    16. }

    Like AST nodes they also have a start, end, and loc.

    Syntactic Analysis" class="reference-link">Syntactic Analysis

    Syntactic Analysis will take a stream of tokens and turn it into an AST representation. Using the information in the tokens, this phase will reformat them as an AST which represents the structure of the code in a way that makes it easier to work with.

    Transform" class="reference-link">Transform

    The transform stage takes an AST and traverses through it, adding, updating, and removing nodes as it goes along. This is by far the most complex part of Babel or any compiler. This is where plugins operate and so it will be the subject of most of this handbook. So we won’t dive too deep right now.

    Generate" class="reference-link">Generate

    The code generation) stage takes the final AST and turns it back into a string of code, also creating source maps.

    Code generation is pretty simple: you traverse through the AST depth-first, building a string that represents the transformed code.

    Traversal" class="reference-link">Traversal

    When you want to transform an AST you have to traverse the tree recursively.

    Say we have the type FunctionDeclaration. It has a few properties: id, params, and body. Each of them have nested nodes.

    1. {
    2. type: "FunctionDeclaration",
    3. id: {
    4. type: "Identifier",
    5. name: "square"
    6. },
    7. params: [{
    8. type: "Identifier",
    9. name: "n"
    10. }],
    11. body: {
    12. type: "BlockStatement",
    13. body: [{
    14. type: "ReturnStatement",
    15. argument: {
    16. type: "BinaryExpression",
    17. operator: "*",
    18. left: {
    19. type: "Identifier",
    20. name: "n"
    21. },
    22. right: {
    23. type: "Identifier",
    24. name: "n"
    25. }
    26. }
    27. }]
    28. }
    29. }

    So we start at the FunctionDeclaration and we know its internal properties so we visit each of them and their children in order.

    Next we go to id which is an Identifier. Identifiers don’t have any child node properties so we move on.

    After that is params which is an array of nodes so we visit each of them. In this case it’s a single node which is also an Identifier so we move on.

    Then we hit body which is a BlockStatement with a property body that is an array of Nodes so we go to each of them.

    The only item here is a ReturnStatement node which has an argument, we go to the argument and find a BinaryExpression.

    The BinaryExpression has an operator, a left, and a right. The operator isn’t a node, just a value, so we don’t go to it, and instead just visit left and right.

    This traversal process happens throughout the Babel transform stage.

    Visitors" class="reference-link">Visitors

    When we talk about “going” to a node, we actually mean we are visiting them. The reason we use that term is because there is this concept of a visitor.

    Visitors are a pattern used in AST traversal across languages. Simply put they are an object with methods defined for accepting particular node types in a tree. That’s a bit abstract so let’s look at an example.

    1. const MyVisitor = {
    2. Identifier() {
    3. console.log("Called!");
    4. }
    5. };
    6. // You can also create a visitor and add methods on it later
    7. let visitor = {};
    8. visitor.MemberExpression = function() {};
    9. visitor.FunctionDeclaration = function() {}

    Note: Identifier() { ... } is shorthand for Identifier: { enter() { ... } }.

    This is a basic visitor that when used during a traversal will call the Identifier() method for every Identifier in the tree.

    So with this code the Identifier() method will be called four times with each Identifier (including square).

    1. function square(n) {
    2. return n * n;
    3. }
    1. path.traverse(MyVisitor);
    2. Called!
    3. Called!
    4. Called!
    5. Called!

    These calls are all on node enter. However there is also the possibility of calling a visitor method when on exit.

    Imagine we have this tree structure:

    1. - FunctionDeclaration
    2. - Identifier (id)
    3. - Identifier (params[0])
    4. - BlockStatement (body)
    5. - ReturnStatement (body)
    6. - BinaryExpression (argument)
    7. - Identifier (left)
    8. - Identifier (right)

    As we traverse down each branch of the tree we eventually hit dead ends where we need to traverse back up the tree to get to the next node. Going down the tree we enter each node, then going back up we exit each node.

    Let’s walk through what this process looks like for the above tree.

    • Enter FunctionDeclaration
      • Enter Identifier (id)
        • Hit dead end
      • Exit Identifier (id)
      • Enter Identifier (params[0])
        • Hit dead end
      • Exit Identifier (params[0])
      • Enter BlockStatement (body)
        • Enter ReturnStatement (body)
          • Enter BinaryExpression (argument)
            • Enter Identifier (left)
              • Hit dead end
            • Exit Identifier (left)
            • Enter Identifier (right)
              • Hit dead end
            • Exit Identifier (right)
          • Exit BinaryExpression (argument)
        • Exit ReturnStatement (body)
      • Exit BlockStatement (body)
    • Exit FunctionDeclaration

    So when creating a visitor you have two opportunities to visit a node.

    1. const MyVisitor = {
    2. Identifier: {
    3. enter() {
    4. console.log("Entered!");
    5. },
    6. exit() {
    7. console.log("Exited!");
    8. }
    9. }
    10. };

    If necessary, you can also apply the same function for multiple visitor nodes by separating them with a | in the method name as a string like Identifier|MemberExpression.

    Example usage in the flow-comments plugin

    1. const MyVisitor = {
    2. "ExportNamedDeclaration|Flow"(path) {}
    3. };

    You can also use aliases as visitor nodes (as defined in babel-types).

    For example,

    Function is an alias for FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod.

    1. const MyVisitor = {
    2. Function(path) {}
    3. };

    Paths" class="reference-link">Paths

    An AST generally has many Nodes, but how do Nodes relate to one another? We could have one giant mutable object that you manipulate and have full access to, or we can simplify this with Paths.

    A Path is an object representation of the link between two nodes.

    For example if we take the following node and its child:

    1. {
    2. type: "FunctionDeclaration",
    3. id: {
    4. type: "Identifier",
    5. name: "square"
    6. },
    7. ...
    8. }

    And represent the child Identifier as a path, it looks something like this:

    1. {
    2. "parent": {
    3. "type": "FunctionDeclaration",
    4. "id": {...},
    5. ....
    6. },
    7. "node": {
    8. "type": "Identifier",
    9. "name": "square"
    10. }
    11. }

    It also has additional metadata about the path:

    1. {
    2. "parent": {...},
    3. "node": {...},
    4. "hub": {...},
    5. "contexts": [],
    6. "data": {},
    7. "shouldSkip": false,
    8. "shouldStop": false,
    9. "removed": false,
    10. "state": null,
    11. "opts": null,
    12. "skipKeys": null,
    13. "parentPath": null,
    14. "context": null,
    15. "container": null,
    16. "listKey": null,
    17. "inList": false,
    18. "parentKey": null,
    19. "key": null,
    20. "scope": null,
    21. "type": null,
    22. "typeAnnotation": null
    23. }

    As well as tons and tons of methods related to adding, updating, moving, and removing nodes, but we’ll get into those later.

    In a sense, paths are a reactive representation of a node’s position in the tree and all sorts of information about the node. Whenever you call a method that modifies the tree, this information is updated. Babel manages all of this for you to make working with nodes easy and as stateless as possible.

    Paths in Visitors" class="reference-link">Paths in Visitors

    When you have a visitor that has a Identifier() method, you’re actually visiting the path instead of the node. This way you are mostly working with the reactive representation of a node instead of the node itself.

    1. const MyVisitor = {
    2. Identifier(path) {
    3. console.log("Visiting: " + path.node.name);
    4. }
    5. };
    1. a + b + c;
    1. path.traverse(MyVisitor);
    2. Visiting: a
    3. Visiting: b
    4. Visiting: c

    State" class="reference-link">State

    State is the enemy of AST transformation. State will bite you over and over again and your assumptions about state will almost always be proven wrong by some syntax that you didn’t consider.

    Take the following code:

    1. function square(n) {
    2. return n * n;
    3. }

    Let’s write a quick hacky visitor that will rename n to x.

    1. let paramName;
    2. const MyVisitor = {
    3. FunctionDeclaration(path) {
    4. const param = path.node.params[0];
    5. paramName = param.name;
    6. param.name = "x";
    7. },
    8. Identifier(path) {
    9. if (path.node.name === paramName) {
    10. path.node.name = "x";
    11. }
    12. }
    13. };

    This might work for the above code, but we can easily break that by doing this:

    1. function square(n) {
    2. return n * n;
    3. }
    4. n;

    The better way to deal with this is recursion. So let’s make like a Christopher Nolan film and put a visitor inside of a visitor.

    1. const updateParamNameVisitor = {
    2. Identifier(path) {
    3. if (path.node.name === this.paramName) {
    4. path.node.name = "x";
    5. }
    6. }
    7. };
    8. const MyVisitor = {
    9. FunctionDeclaration(path) {
    10. const param = path.node.params[0];
    11. const paramName = param.name;
    12. param.name = "x";
    13. path.traverse(updateParamNameVisitor, { paramName });
    14. }
    15. };
    16. path.traverse(MyVisitor);

    Of course, this is a contrived example but it demonstrates how to eliminate global state from your visitors.

    Scopes" class="reference-link">Scopes

    Next let’s introduce the concept of a scope). JavaScript has lexical scoping#Lexical_scoping_vs._dynamic_scoping), which is a tree structure where blocks create new scope.

    1. // global scope
    2. function scopeOne() {
    3. // scope 1
    4. function scopeTwo() {
    5. // scope 2
    6. }
    7. }

    Whenever you create a reference in JavaScript, whether that be by a variable, function, class, param, import, label, etc., it belongs to the current scope.

    1. var global = "I am in the global scope";
    2. function scopeOne() {
    3. var one = "I am in the scope created by `scopeOne()`";
    4. function scopeTwo() {
    5. var two = "I am in the scope created by `scopeTwo()`";
    6. }
    7. }

    Code within a deeper scope may use a reference from a higher scope.

    1. function scopeOne() {
    2. var one = "I am in the scope created by `scopeOne()`";
    3. function scopeTwo() {
    4. one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
    5. }
    6. }

    A lower scope might also create a reference of the same name without modifying it.

    1. function scopeOne() {
    2. var one = "I am in the scope created by `scopeOne()`";
    3. function scopeTwo() {
    4. var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
    5. }
    6. }

    When writing a transform, we want to be wary of scope. We need to make sure we don’t break existing code while modifying different parts of it.

    We may want to add new references and make sure they don’t collide with existing ones. Or maybe we just want to find where a variable is referenced. We want to be able to track these references within a given scope.

    A scope can be represented as:

    1. {
    2. path: path,
    3. block: path.node,
    4. parentBlock: path.parent,
    5. parent: parentScope,
    6. bindings: [...]
    7. }

    When you create a new scope you do so by giving it a path and a parent scope. Then during the traversal process it collects all the references (“bindings”) within that scope.

    Once that’s done, there’s all sorts of methods you can use on scopes. We’ll get into those later though.

    Bindings" class="reference-link">Bindings

    References all belong to a particular scope; this relationship is known as a binding.

    1. function scopeOnce() {
    2. var ref = "This is a binding";
    3. ref; // This is a reference to a binding
    4. function scopeTwo() {
    5. ref; // This is a reference to a binding from a lower scope
    6. }
    7. }

    A single binding looks like this:

    1. {
    2. identifier: node,
    3. scope: scope,
    4. path: path,
    5. kind: 'var',
    6. referenced: true,
    7. references: 3,
    8. referencePaths: [path, path, path],
    9. constant: false,
    10. constantViolations: [path]
    11. }

    With this information you can find all the references to a binding, see what type of binding it is (parameter, declaration, etc.), lookup what scope it belongs to, or get a copy of its identifier. You can even tell if it’s constant and if not, see what paths are causing it to be non-constant.

    Being able to tell if a binding is constant is useful for many purposes, the largest of which is minification.

    1. function scopeOne() {
    2. var ref1 = "This is a constant binding";
    3. becauseNothingEverChangesTheValueOf(ref1);
    4. function scopeTwo() {
    5. var ref2 = "This is *not* a constant binding";
    6. ref2 = "Because this changes the value";
    7. }
    8. }