Home

Awesome

This package currently doesn't support React Native

This preset simplifies Babel configuration for modern JavaScript we use at Adeira.

Installation and Usage

Install this package:

yarn add --dev @babel/core @adeira/babel-preset-adeira

And use it in your babel.config.js:

module.exports = function (api) {
  api.assertVersion(7);
  api.cache.forever();

  const presets = ['@adeira/babel-preset-adeira'];
  const extraPlugins = []; // add whatever you want extra

  return {
    presets,
    plugins: extraPlugins,
  };
};

Configuration

This preset tries to be opinionated to shield you from the difficult configuration stuff, but it offers some configuration options, so you can target different environments and situations (modern browsers vs. Node.js vs. Flow-only envs and so on). Current default configuration is:

const config = {
  target: 'js',
  environments: {
    node: 'current',
    browsers: ['defaults'], // See:  npx browserslist 'defaults'
  },
};

This should be okay for the majority of the use-cases. However, you can tweak these options as needed (continue below).

Transpilation targets

There are three transpilation targets available: js (default), js-esm and flow. JavaScript transpilation target adds these features:

On top of that these proposals are enabled by default:

JS-ESM variant is doing the same except it's targeting modern JS environments which support ES6 modules (import/export).

This preset uses env preset behind the scenes which means that by default it transpiles JS to the current Node.js version you are running. Therefore, it's recommended to do the transpilation in your Docker container that is identical to your production version. On top of that it transpiles code based on the Browserlist Best Practices (can be changed via environments option). You can also choose Flow as a transpilation target. Flow target uses only these features:

Use option target to enable Flow-only transpilations:

module.exports = function (api) {
  api.assertVersion(7);
  api.cache.forever();

  return {
    presets: [
      [
        '@adeira/babel-preset-adeira',
        {
          target: 'flow', // or 'js' (default) or 'js-esm'
        },
      ],
    ],
  };
};

What is the difference between these transpilation targets? JavaScript target transpiles your code, so it can run in any Node.js and browsers environment with the modern JS features whereas Flow only tweaks the exported types, so they can be used in different projects (but leaves JS code as is, think better flow-copy-source).

It's also easily possible to change your target based on your Babel runner. It's handy when you need to support SSR with CommonJS as well as ESM:

function isWebpack(caller) {
  return !!(caller && caller.name === 'babel-loader'); // See: https://github.com/babel/babel-loader
}

module.exports = function (api) {
  api.assertVersion(7);

  return {
    presets: [
      [
        '@adeira/babel-preset-adeira',
        {
          target: api.caller(isWebpack) ? 'js-esm' : 'js',
        },
      ],
    ],
  };
};

Note that the above-mentioned is also necessary to enable code-splitting with import(…). The default (if no target is set), will be commonjs, and it will not code split your dynamic imports.

Different environments

While transpilation targets are targeting different JavaScript systems (ES6, Flow), you can also change environments (Browsers, Node.js, Electron). To do so you can use environments configuration:

module.exports = {
  presets: [
    [
      '@adeira/babel-preset-adeira',
      {
        environments: {
          node: 'current', // targeting only current Node.js version (no browsers)
        },
      },
    ],
  ],
};

Please note: environments and transpilation targets are 2 distinct features. Transpilation targets allow you to modify what kind of code you want to get as a result of compilation (JS, JS with modules, Flow types) while environments allow you to enable/disable tranpilation features based on your environment. For example: common use-case it to use js-esm tranpilation target so that Webpack can perform tree-shaking but environment is set to old browsers only. This means that Webpack can do the magic thanks to import/export however, final code will work in old browsers because Webpack is going to merge everything together and effectively remove these ES6 imports. Few more examples to understand the difference between transpilation targets and environments:

You can easily debug what environments and plugins are being used when you pass debug:true option to our Babel preset options:

module.exports = {
  presets: [
    [
      '@adeira/babel-preset-adeira',
      {
        debug: true,
      },
    ],
  ],
};

You should be able to see similar output (default values to this date for adeira/universe):

Using targets:
{
  "android": "90",
  "chrome": "89",
  "edge": "90",
  "firefox": "78",
  "ie": "11",
  "ios": "13.4",
  "node": "16.8",
  "opera": "75",
  "safari": "14",
  "samsung": "13"
}

Using modules transform: commonjs

Using plugins:
  proposal-private-property-in-object { android, chrome < 91, edge, firefox < 90, ie, ios, node, opera, safari, samsung }
  proposal-class-properties { firefox < 90, ie, ios, safari < 15, samsung }
  proposal-private-methods { firefox < 90, ie, ios, safari < 15, samsung }
  proposal-numeric-separator { ie }
  proposal-logical-assignment-operators { firefox < 79, ie, ios < 14, samsung }
  proposal-nullish-coalescing-operator { ie }
  proposal-optional-chaining { ie }
  proposal-json-strings { ie }
  proposal-optional-catch-binding { ie }
  transform-parameters { ie }
  proposal-async-generator-functions { ie }
  proposal-object-rest-spread { ie }
  transform-dotall-regex { ie }
  proposal-unicode-property-regex { ie }
  transform-named-capturing-groups-regex { ie }
  transform-async-to-generator { ie }
  transform-exponentiation-operator { ie }
  transform-template-literals { ie }
  transform-literals { ie }
  transform-function-name { ie }
  transform-arrow-functions { ie }
  transform-classes { ie }
  transform-object-super { ie }
  transform-shorthand-properties { ie }
  transform-duplicate-keys { ie }
  transform-computed-properties { ie }
  transform-for-of { ie }
  transform-sticky-regex { ie }
  transform-unicode-escapes { ie }
  transform-unicode-regex { ie }
  transform-spread { ie }
  transform-destructuring { ie }
  transform-block-scoping { ie }
  transform-typeof-symbol { ie }
  transform-new-target { ie }
  transform-regenerator { ie }
  proposal-export-namespace-from { firefox < 80, ie, ios, safari }
  transform-modules-commonjs
  proposal-dynamic-import

Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.

New JSX transform

This packages uses the new JSX transform as default from 2.0.0 (see https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). Optional support for the new JSX transform was added in 1.2.0.

If you want to use the classical version with this package from 2.0.0 (having import React from 'react'; in all files) you can use it like this:

module.exports = {
  presets: [
    [
      '@adeira/babel-preset-adeira',
      {
        reactRuntime: 'classic',
      },
    ],
  ],
};

If you want to use the new JSX transform, you don't need to explicitly set reactRuntime: 'automatic'.

Transpilation features explained

We want to make our life easier by writing modern JS and this Babel preset helps us with that. It's tailored for needs of Adeira, and it brings mostly syntactic sugar into our JS code. However, there are some additional features which are not related to JS syntax:

__DEV__ expression

Expression __DEV__ is a great way how to detect development environment while keeping this check multiplatform (process.env doesn't exist in React Native for example). It's a boolean value and it is set to true in dev (and test) environments, but it's false in production. It's useful when you want to call something only for development:

if (__DEV__) {
  console.log('This is relevant only when developing the application.');
}

These expressions should be stripped out with dead-code elimination when building your application for production.

Please note: it's invalid to redeclare this expression with a custom value just like it's invalid to redeclare true or false. It's because it transpiles to process.env call and it would result in invalid JS code:

__DEV__ = true;

//   ↓ ↓ ↓

process.env.NODE_ENV !== 'production' = true; // ??? 🧐

Our Eslint config can help you to catch such mistakes. You'll get an error in newer versions of this preset:

Error: You are trying to re-declare __DEV__ virtual constant but that's illegal.
This constant is being defined by our Babel preset.

Read more: How Does the Development Mode Work?

invariant and warning functions

Functions invariant and warning are useful when you want to express some unexpected application state or show warnings with helpful messages. They work without any presets because they are normal functions (using the __DEV__ expression behind the scenes) however, they add unnecessary code to the production build. This preset adds additional transformation which can remove this unnecessary code:

import { invariant } from '@adeira/js';
invariant(condition, argument, argument);

//     ↓ ↓ ↓ ↓ ↓ ↓

import { invariant } from '@adeira/js';
if (!condition) {
  // this is essentially __DEV__ (vv)
  if (process.env.NODE_ENV !== 'production') {
    invariant(false, argument, argument);
  } else {
    invariant(false);
  }
}

Technically, it looks like this final code is larger than the input. We expect that you use dead-code elimination so the final build is actually reduced to minimum. The same applies to warning except in this case the final output can be eliminated completely:

import { warning } from '@adeira/js';
warning(condition, argument, argument);

//     ↓ ↓ ↓ ↓ ↓ ↓

import { warning } from '@adeira/js';
// __DEV__ (vv)
if (process.env.NODE_ENV !== 'production') {
  warning(condition, argument, argument);
}

Prior art