Conditional compilation for JavaScript using Babel

Conditional compilation is not a very common topic for JavaScript. Most JavaScript code runs on the web, where the same piece of code has to work on a range of different browser platforms. For conditional code execution we usually check values that are determined at runtime (Browser API, Web Requests).

However there are cases where it can be beneficial to use conditional compilation instead.

Traditionally it is a technique that is often used is when compiling native executables for different platforms or architectures. In this scenario you want the executable to only include instructions that are available on that platform.

While JavaScript does not need to be compiled into native instructions, there are still use cases where we can benefit from conditional compilation:

  • Reduce load times — Conditional compilation does not only control code execution, but it actually removes code instead of just not executing it. This can help in reducing the size of the code and initial load times for web applications. For example we can remove debug or testing code that is not needed in production.
  • Platform-specific builds — With increasing usage of JavaScript for native Apps on desktop (Electron, nw.js) and mobile (React Native, NativeScript) it again becomes a common use case to create several versions of the same app for different platforms. In this case we may conditionally include code that accesses a native feature that is only available on a specific platform.
  • Feature Toggle — With feature toggles you do not develop features in a separate feature branch, but use the main branch instead to avoid complex merges of long-running branches. To prevent users from accessing unfinished features you have a configuration option (“toggle”) that determines whether a feature is active. These toggles can be implemented using conditional compilation. We can remove the code that calls the feature and we could also remove the code for the feature itself.
  • Feature Sets — This is similar to features toggles, but does toggle a complete set of features. For example you might have a setup where you create a free and a paid version of your app from the same code base. With conditional compilation you can remove paid features from the free version, thus preventing the possibility of unlocking the paid version from a free version.

In this article we are going to have a look at how we can implement conditional compilation using the popular JavaScript preprocessor Babel. First we’ll have a look at how conditional compilation could work in JavaScript. Then we implement a solution using an existing Babel plugin. At the end of the article I’m going to list some alternative solutions.

Solution outline

Let’s have a look at the program that we want to compile:

console.log('Start');
if (TARGET == 'development') {
console.log('Development build');
}
console.log('End');

This is a simple program that contains some log statements. One statement is wrapped in a conditional (if statement) and only executes if TARGET has the value development.

What we want to achieve is to compile two different versions of that program depending on which value the environment variable TARGET has during compilation.

If we set the TARGET variable to development we want to create a development build that should execute the log statement:

console.log('Start');
console.log('Development build');
console.log('End');

If we set the TARGET variable to any other value, we want to create a production build that does not execute the log statement:

console.log('Start');
console.log('End');

To make this work we have to find a way to evaluate the condition at compile-time. Depending on the result we either include or exclude the log statement in the resulting code. So our solution would need to execute the following steps:

  • Inline the TARGET variable, which means we replace the expression with a constant value that can be evaluated at compile-time
  • Evaluate the condition
  • If the condition evaluates to true, replace the conditional with the if branch
  • If the condition evaluates to false, replace the conditional with the else branch, or remove it completely if there is no else branch

Let’s run this through with an example. Assuming the variable TARGET is set to development then inlining the variable would produce the following output:

if ('development' == 'development') {
console.log('Development build');
}

Now we can statically evaluate the condition which should evaluate to true:

if (true) {
console.log('Development build');
}

Because the condition always evaluates to true, we replace the conditional with the ifbranch:

console.log('Development build');

Which is the output that we would expect for our development build. In the next section we are going to implement our solution.

Implementation

We could implement our own Babel plugin to illustrate how the individual code transforms work, however that would involve explaining a lot of details about the Babel plugin architecture which is beyond the scope of this article. Instead we are going to use an existing plugin called babel-plugin-conditional-compile which does pretty much what we want. The plugin allows us to configure which expressions should be inlined during compilation. It then searches for conditionals that can be statically evaluated and then simplifies them by substituting them with either the if or the else branch.

To start off we create a new folder and name it babel-conditional-compilation.

Within the folder we create a basic package.json file:

npm init -y

Then we can install Babel and the relevant plugin:

npm install --save-dev babel-cli
npm install --save-dev babel-plugin-conditional-compile

Next we create a Babel configuration file called .babelrc and add the following contents:

{
"plugins": [
["conditional-compile", {
"define": {
"TARGET": "development"
}
}]
]
}

This configures the plugin to replace the TARGET expression with the string development.

We also need our example program, which we store in a file called example.js:

console.log('Start');
if (TARGET == 'development') {
console.log('Development build');
}
console.log('End');

To run Babel we add a npm script to the package.json that was created earlier:

{
...
"scripts": {
"build": "babel example.js -d dist"
}
...
}

Now we can run Babel using:

npm run build

This should create a folder named dist in which we find the processed example.js file. Looking at it we can see that that the development version of the program was built:

console.log('Start');
console.log('Development build');
console.log('End');

Let’s change the .babelrc configuration to produce the production build:

{
"plugins": [
["conditional-compile", {
"define": {
"TARGET": "production"
}
}]
]
}

And run Babel again:

npm run build

The processed example.js should now contain the production build:

console.log('Start');
console.log('End');

With this setup we now have a working solution for conditional compilation.

What’s positive about this solution is that we can use standard if conditionals to solve conditional compilation. We do not need to introduce new syntax or code constructs that might confuse linters or IDEs.

However this is also its downside, because these conditionals look really innocent. Looking at the code you can not tell if the conditional is going to be evaluated at compile-time or during runtime. This can be somewhat mitigated by using special variable names.

Another problem is code readability. If you need a lot of conditionals it can make the code hard to read. Instead of littering up your code with conditionals it might be better to extract that code into a separate module and then provide different implementations for it.

In the final section I’m going to list some alternatives to the Babel plugin that we used in our solution.

Alternatives

babel-plugin-conditional-compilation

Another Babel plugin that uses C/C++ style macros. It uses JavaScript strings that contain macro directives to wrap code that should be conditionally compiled:

"#if DEBUG > 1";
DEBUG = 1;
"#endif";

It has the benefit that these strings with macro directives are more visible than a simple if conditional, it requests your attention that something special is going on here. The downside is that it introduces additional syntax and is harder to read.

UglifyJS

UglifyJS also supports conditional compilation. However at the time of this writing, UglifyJS still does not support ES 2015 or above, which means you might still need to use Babel to transpile your code to ES5 beforehand.