npm package: @rnx-kit/metro-serializer-esbuild
Description: esbuild serializer for Metro
Author: Microsoft Open Source
Latest version: 0.3.1

# @rnx-kit/metro-serializer-esbuild

[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/@rnx-kit/metro-serializer-esbuild)](https://www.npmjs.com/package/@rnx-kit/metro-serializer-esbuild)
![Stability Beta](https://img.shields.io/badge/Stability-Beta-3bf)

Allow Metro to use [esbuild](https://esbuild.github.io) for bundling and
serialization.

This tool is in Beta, and has been yielding good results so far. See the list of
known issues below for more information.

## Motivation

Metro currently does not implement tree shaking, i.e. it does not attempt to
remove unused code from the JS bundle. For instance, given this code snippet:

```ts
import { partition } from "lodash";
```

Metro will bundle all of `lodash` in your bundle even though you're only using a
small part of it. In `lodash`'s case, you can add
[`babel-plugin-lodash`](https://github.com/lodash/babel-plugin-lodash#readme) to
your Babel config to help Metro strip away some modules, but not all libraries
will come with such helpers. For more details, see issues
[#227](https://github.com/facebook/metro/issues/227) and
[#632](https://github.com/facebook/metro/issues/632).

metro-serializer-esbuild addresses this by letting esbuild take over bundling.

## Requirements

This plugin currently depends on some unstable features introduced in Metro
[0.66.1](https://github.com/facebook/metro/releases/tag/v0.66.1). Breaking
changes were introduced in Metro 0.60, so this plugin will not work with React
Native below 0.64.

## Usage

esbuild works best when we pass it ES6 modules. The first thing we must do is to
disable import/export transformation by enabling `disableImportExportTransform`
in `babel.config.js`:

```diff
+const env = process.env.BABEL_ENV || process.env.NODE_ENV;
 module.exports = {
   presets: [
     [
       "module:@react-native/babel-preset",
+      {
+        disableImportExportTransform:
+          env === "production" && process.env["RNX_METRO_SERIALIZER_ESBUILD"],
+      },
     ],
   ],
 };
```

To avoid issues with dev server, we only want to enable
`disableImportExportTransform` when bundling for production.

If you're using `@rnx-kit/babel-preset-metro-react-native`, you don't need to
make any changes.

> Note that Hermes currently does not fully implement the
> [ES6 spec](https://kangax.github.io/compat-table/es6/). esbuild, on the other
> hand, does not fully support
> [lowering to ES5](https://github.com/evanw/esbuild/issues/297). This
> essentially means that you may have to add additional plugins if you're seeing
> esbuild outputting "target environment is not supported yet" errors during
> bundle. For an example, see the error and its solution in
> [#1743](https://github.com/microsoft/rnx-kit/issues/1743).

Next, configure Metro to use the esbuild serializer by making the following
changes to `metro.config.js`:

```diff
 const { makeMetroConfig } = require("@rnx-kit/metro-config");
+const {
+  MetroSerializer,
+  esbuildTransformerConfig,
+} = require("@rnx-kit/metro-serializer-esbuild");

 module.exports = makeMetroConfig({
   serializer: {
+    customSerializer: MetroSerializer(),
   },
+  transformer: esbuildTransformerConfig,
 });
```

> Note that `esbuildTransformerConfig` is incompatible with dev server and debug
> builds. It should only be set when bundling for production.

We can now create a bundle as usual, e.g.:

```sh
react-native bundle --entry-file index.js --platform ios --dev false ...
```

## Options

Similar to
[`metro-serializer`](https://github.com/microsoft/rnx-kit/tree/main/packages/metro-serializer#usage),
`metro-serializer-esbuild` also supports plugins. Additionally, you can
configure the output of the plugin by passing an options object as the second
parameter. For instance, to output ES6 compliant code, set the target option:

```diff
 const myPlugins = [...];
 module.exports = makeMetroConfig({
   serializer: {
     customSerializer: MetroSerializer(myPlugins, {
+      target: "es6"
     }),
   },
   transformer: esbuildTransformerConfig,
 });
```

Below are all the currently supported options.

### `target`

Sets the target environment for the transpiled JavaScript code.

See the full documentation at https://esbuild.github.io/api/#target.

Values: Any JS language version string such as `es6` or `esnext`. You can also
use environment names. See the full documentation for a list of supported names.

Defaults to `hermes0.7.0`.

### `fabric`

When enabled, includes Fabric-enabled version of React. You can save some bytes
by disabling this if you haven't migrated to Fabric yet.

Defaults to `true`.

### `drop`

Tells esbuild to edit your source code before building to drop certain
constructs. There are currently two possible things that can be dropped:
`debugger` and `console`.

See the full documentation at https://esbuild.github.io/api/#drop.

By default, this option is not set.

### `minify`

When enabled, the generated code will be minified instead of pretty-printed.

See the full documentation at https://esbuild.github.io/api/#minify.

Defaults to `true` in production environment; `false` otherwise.

### `minifyWhitespace`

Same as `minify` but only removes whitespace.

See the full documentation at https://esbuild.github.io/api/#minify.

By default, this option is not set.

### `minifyIdentifiers`

Same as `minify` but only renames local variables to be shorter.

See the full documentation at https://esbuild.github.io/api/#minify.

By default, this option is not set.

### `minifySyntax`

Same as `minify` but only rewrites syntax to be more compact.

See the full documentation at https://esbuild.github.io/api/#minify.

By default, this option is not set.

### `pure`

Add `/* @__PURE__ */` annotation to the specified new or call expressions. This
tells esbuild they can be removed if the resulting value is unused.

See the full documentation at https://esbuild.github.io/api/#pure.

By default, this option is not set.

### `sourceMapPaths`

Determines whether paths in the output source map are absolute or relative to
the directory containing the source map.

Values: `absolute` | `relative`

Defaults to `relative`.

### `strictMode`

By default, the `"use strict";` directive is added by Babel and esbuild when
lowering to ES5. You can save some bytes by telling this serializer to strip
them from the bundle.

Note that disabling `strictMode` here will definitely break source maps. It is
recommended to try disabling strict mode in Babel or TypeScript first before
considering this option. If you can target ES6, that is a better alternative.

This flag only affects production environment.

Defaults to `true`.

### `analyze`

Sets whether esbuild should output a report at the end of bundling.

See the full documentation at https://esbuild.github.io/api/#analyze.

Values: `false` | `true` | `verbose`

Defaults to `false`.

### `logLevel`

The log level passed to esbuild.

See the full documentation at https://esbuild.github.io/api/#log-level

Values: `verbose` | `debug` | `info` | `warning` | `error` | `silent`

Defaults to `warning`.

### `metafile`

The path to write metadata to, relative to the package root.

Determines whether esbuild should produce some metadata about the build in JSON
format.

See the full documentation at https://esbuild.github.io/api/#metafile.

## Metro + ESM Support

Metro currently does not support ESM. However, if you're looking to save even
more bytes, and are comfortable with solving CJS vs ESM resolution issues, you
can try adding `module` to
[`resolver.resolverMainFields`](https://facebook.github.io/metro/docs/configuration#resolvermainfields)
in `metro.config.js`. This will tell Metro to always pick ESM over CJS when
possible. Note that this can lead to unexpected errors since you cannot import
ESM from CJS. Until https://github.com/facebook/metro/issues/670 lands, you are
basically on your own to fix any issues that might come up.

## Known Limitations

- Dev server does not play well with `esbuildTransformerConfig`. To work around
  this limitation, you can save the esbuild specific Metro config to a separate
  file and only specify it when needed, e.g.:
  ```sh
  react-native bundle ... --config metro+esbuild.config.js
  ```
- esbuild does not properly tree-shake `export *`. This is a known limitation
  (see https://github.com/evanw/esbuild/issues/1420). It is also not recommended
  to use `export *` in your code as they may lead to duplicate exports. For more
  details, read https://hackmd.io/Z021hgSGStKlYLwsqNMOcg. This can be mitigated
  with an ESLint rule, such as `no-export-all` from
  [`@rnx-kit/eslint-plugin`](https://github.com/microsoft/rnx-kit/tree/main/packages/eslint-plugin#readme).
- esbuild is incompatible with
  [RAM bundle](https://facebook.github.io/metro/docs/bundling/#indexed-ram-bundle).
  If you require RAM bundles, you cannot use this serializer. In fact, Metro
  will simply ignore it.