Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

Replacing code for production with Babel transformation

Replacing code for production with Babel transformation Photo by Chris Lawton

If you are building some sort of an app, Babel is probably part of your build system. It is that thing which converts our fancy code to valid, working in a browser, JavaScript. Just recently at work I had to design a solution that swaps a class based on the environment. Or in another words, we have logic that should not reach our users. The file should be available locally and on our staging environment but not in production. A tiny Babel plugin was the cheapest solution for me so I decided to share the result.


The bit that I had to touch is the Babel configuration file which in our case is babel.config.js. We are already using Babel plugins so our setup looks like this:

plugins: [
  [
    'babel-plugin-styled-components',
    { ssr: true },
  ],
  [
    'babel-plugin-module-resolver',
    {
      alias,
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      stripExtensions: ['.js', '.jsx', '.ts', '.tsx'],
    },
  ],
  'babel-plugin-jsx-strip-ext',
  'babel-plugin-syntax-async-functions',
  'babel-plugin-dynamic-import-node',
  '@babel/plugin-transform-runtime',
  '@babel/plugin-syntax-dynamic-import',
  '@babel/plugin-syntax-import-meta',
  '@babel/plugin-proposal-class-properties',
  '@babel/plugin-proposal-json-strings',
  '@babel/plugin-transform-classes'
]

To add a new plugin we don't need to define an external module. Because we are using .js file we can simply place the plugin implementation directly into babel.config.js. And a plugin really is a function that returns object with a visitor map. For example:

const plugin = () => ({
  visitor: {}
})

Then in that visitor field we have to list the AST nodes that we want to modify. AST stands for abstract syntax tree. In JavaScript this is basically a tree liked object that statically represents our code. Our visitor function is called once per each node that matches the defined type. For example if we want to catch all the string literals in our file we have to use:

visitor: {
  StringLiteral: (path) => {
    // ...
  }
}

So, it is all about seeing how your tree looks like (check out AST explorer here) and write a proper visitor. The code that I had to change was an import statement. Something like:

import Service from 'company-lib/services/Service';

On the local and staging environment we wanted to use a slightly different version of the same service. So, the path should be changed only when building for those two environments.

The AST for such statements looks like this:

{
  "type": "ImportDeclaration",
  "specifiers": [
    {
      "type": "ImportDefaultSpecifier",
      "local": {
        "type": "Identifier",
        "name": "Service"
      }
    }
  ],
  "importKind": "value",
  "source": {
    "type": "StringLiteral",
    "extra": {
      "rawValue": "company-lib/services/Service",
      "raw": "'company-lib/services/Service'"
    },
    "value": "company-lib/services/Service"
  }
}

We want to visit a node of type ImportDeclaration and check if the ImportDefaultSpecifier's name is Service and the path is company-lib/services/Service. If that is true then we change the source's value field with the desired path.

Here's the final version of the plugin:

const plugin = () => ({
  visitor: {
    ImportDeclaration(path){
      if (
        path.node.specifiers[0].local.name === "Service" &&
        path.node.source.value === 'company-lib/services/Service'
      ) {
        path.node.source.value = './local/version/Service';
      }
    }
  }
});

// Other Babel options
  ...
  plugins: [
    plugin,
    [
      'babel-plugin-styled-components',
      { ssr: true },
    ],
    [
      'babel-plugin-module-resolver',
      {
        alias,
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
        stripExtensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    ],
    ...
  ]

If you decide to use this approach here are a couple of notes:

  • Always write your code for prod and make changes on top of it. When you get local/staging versions as a starting point there is a bigger chance to publish something unwanted.
  • Be as specific as possible when looking for the code subject of change.
  • You probably want to use a utility like Lodash's get method when accessing items of the node.
  • You may want to try your transformations in this Babel plugin playground. A lot cheaper than spinning your own locally.

Happy transforming 🦾

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.