Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions incubator/esbuild-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# @rnx-kit/esbuild-service

[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

### This tool is EXPERIMENTAL - USE WITH CAUTION

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

A Metro-independent, esbuild-based bundler for React Native.

## Motivation: Metro vs. esbuild

[Metro](https://facebook.github.io/metro/) is the standard bundler for React
Native. It is reliable, battle-tested, and deeply integrated into the React
Native toolchain. However, Metro was designed around CommonJS semantics and
Babel transformations. This makes it slower at scale and harder to integrate
with modern tooling.

[esbuild](https://esbuild.github.io/) is an extremely fast JavaScript bundler
written in Go. It handles TypeScript and JSX natively, provides excellent tree-
shaking, and produces source maps with minimal overhead.

This package explores using esbuild as a **complete replacement for Metro**
rather than just its serialization step (which is what
[`@rnx-kit/metro-serializer-esbuild`](../packages/metro-serializer-esbuild)
does).

---

## Metro component analysis

The table below maps each Metro component to its esbuild equivalent and
explains how much code must be reimplemented.

| Metro component | Can esbuild replace it? | Notes |
|---|---|---|
| **Transformer** (Babel / Flow) | ✅ Yes — natively | esbuild supports TypeScript and JSX out of the box. Flow types can be stripped with a simple plugin. Babel is no longer needed for the common case. |
| **Dependency graph** | ✅ Yes — natively | esbuild builds its own dependency graph as part of bundling. |
| **Tree-shaking** | ✅ Yes — natively | esbuild performs dead code elimination (DCE) automatically for ESM code. |
| **Minifier** | ✅ Yes — natively | esbuild has a built-in, high-performance minifier. |
| **Source maps** | ✅ Yes — natively | esbuild generates linked or inline source maps. |
| **Serializer** | ✅ Yes — natively | esbuild produces the final bundle; this is the role of `metro-serializer-esbuild`. |
| **Resolver** (platform extensions, `react-native` field) | ⚠️ Plugin required | The `reactNativeResolver` plugin in this package reimplements Metro's platform-extension resolution (`.ios.js`, `.android.js`, `.native.js`) and the `react-native` → `module` → `browser` → `main` field priority from `package.json`. ~250 lines of code. |
| **Pre-modules / polyfills** | ⚠️ Plugin required | The `reactNativePolyfills` plugin reimplements Metro's `preModules` mechanism by injecting a virtual entry-point that sets up `global`, `__DEV__`, and any user-provided polyfills. ~110 lines of code. |
| **Asset handling** | ✅ Plugin included | The `reactNativeAssets` plugin transforms image/font imports into `registerAsset()` calls, reusing Metro's own `getAssetData()` implementation to discover scale variants, compute hashes, and read image dimensions. Asset files are copied to the output directory using `@rnx-kit/metro-service`. ~250 lines of code. |
| **Dev server + HMR** | ❌ Cannot replace | Metro's development server implements React Native's fast-refresh / HMR protocol. esbuild has a basic HTTP server mode but no HMR support. |
| **RAM bundles** | ❌ Cannot replace | Metro's indexed RAM bundle format has no esbuild equivalent. |
| **Lazy module loading** | ❌ Cannot replace | Metro's async require / lazy-loading mechanism requires a custom module loader runtime that esbuild does not provide. |

### Code reuse from `@rnx-kit/metro-serializer-esbuild`

| Component | Reuse? | Notes |
|---|---|---|
| `targets.ts` — Hermes target inference | ✅ Copied | Identical logic; infers the right `hermesX.Y` esbuild target from the installed `react-native` version. |
| `getSideEffects` from `module.ts` | ✅ Concept reused | The `sideEffects` package.json field logic applies equally to a standalone esbuild bundler; esbuild respects it natively through its own side-effects handling. |
| `esbuildTransformerConfig` | ❌ Not applicable | That export configures Metro's Babel transformer to be esbuild-friendly. It is not relevant when Metro is removed entirely. |
| `index.ts` — the custom serializer | ❌ Not applicable | The serializer depends on Metro's dependency graph API and cannot be reused. |
| `sourceMap.ts` — Metro source map helpers | ❌ Not applicable | These helpers wrap Metro's source-map utilities; not needed without Metro. |

---

## Installation

```sh
yarn add --dev @rnx-kit/esbuild-service
```

## Usage

```typescript
import { bundle } from "@rnx-kit/esbuild-service";

await bundle({
entryFile: "index.js",
platform: "ios",
dev: false,
bundleOutput: "dist/main.ios.jsbundle",
sourcemapOutput: "dist/main.ios.jsbundle.map",
// Optional: copy assets to a destination directory
assetsDest: "dist/assets",
});
```

## API

### `bundle(options)`

Bundles a React Native application using esbuild, without Metro.

#### Options

| Option | Type | Default | Description |
|---|---|---|---|
| `entryFile` | `string` | required | Path to the entry file. |
| `platform` | `AllPlatforms` | required | Target platform (`android`, `ios`, `macos`, `windows`, …). |
| `dev` | `boolean` | `false` | Bundle in development mode. |
| `minify` | `boolean` | `!dev` | Minify the output. |
| `bundleOutput` | `string` | required | Path to write the bundle to. |
| `sourcemapOutput` | `string` | — | Path to write the source map to. |
| `assetsDest` | `string` | — | Directory to copy asset files to after bundling. |
| `assetCatalogDest` | `string` | — | iOS asset catalog directory (`RNAssets.xcassets`). |
| `assetDataPlugins` | `string[]` | `[]` | Metro asset data plugins to apply. |
| `target` | `string \| string[]` | Auto-detected | esbuild target (e.g. `"hermes0.12"`). |
| `plugins` | `Plugin[]` | `[]` | Extra esbuild plugins. |
| `projectRoot` | `string` | `process.cwd()` | Project root directory. |
| `logLevel` | esbuild log level | `"warning"` | esbuild log level. |
| `drop` | esbuild drop | — | Drop `debugger` or `console` calls. |
| `pure` | `string[]` | — | Mark calls as side-effect free. |

### `reactNativeResolver(platform, mainFields?)`

An esbuild plugin that adds React Native–specific module resolution:

- Platform-specific file extensions (`.ios.ts`, `.android.ts`, `.native.ts`, …)
- `react-native` → `module` → `browser` → `main` field priority in `package.json`

```typescript
import { reactNativeResolver } from "@rnx-kit/esbuild-service";
import * as esbuild from "esbuild";

await esbuild.build({
entryPoints: ["index.ts"],
bundle: true,
plugins: [reactNativeResolver("ios")],
outfile: "dist/bundle.js",
});
```

### `reactNativePolyfills(options)`

An esbuild plugin that injects React Native globals (`global`, `__DEV__`) and
optional polyfills as a virtual entry-point before your application code.

```typescript
import { reactNativePolyfills } from "@rnx-kit/esbuild-service";
import * as esbuild from "esbuild";

await esbuild.build({
entryPoints: ["index.ts"],
bundle: true,
plugins: [
reactNativePolyfills({
entryFile: "index.ts",
dev: false,
polyfills: ["./polyfills/myPolyfill.js"],
}),
],
outfile: "dist/bundle.js",
});
```

### `reactNativeAssets(options)`

An esbuild plugin that handles React Native asset imports (images, fonts, media
files). It calls Metro's own `getAssetData()` to discover scale variants and
collect metadata, then generates the same `registerAsset()` call that Metro's
asset transformer produces.

The plugin attaches a `getCollectedAssets()` method to retrieve all asset data
gathered during the build, which can be passed to `@rnx-kit/metro-service`'s
`saveAssets()` to copy files to disk.

```typescript
import { reactNativeAssets } from "@rnx-kit/esbuild-service";
import * as esbuild from "esbuild";

const assetsPlugin = reactNativeAssets({
platform: "ios",
projectRoot: process.cwd(),
// Optional: override the asset registry module path
// assetRegistryPath: "@react-native/assets-registry/registry",
});

await esbuild.build({
entryPoints: ["index.ts"],
bundle: true,
plugins: [assetsPlugin],
outfile: "dist/bundle.js",
});

// Retrieve collected AssetData[] to copy files to disk
const assets = assetsPlugin.getCollectedAssets();
```

### `inferBuildTarget(projectRoot?)`

Infers the appropriate esbuild target string for the installed version of
`react-native` / Hermes.

```typescript
import { inferBuildTarget } from "@rnx-kit/esbuild-service";

const target = inferBuildTarget(); // e.g. "hermes0.12"
```

## Known Limitations

- **Dev server / HMR** — use Metro for development; this package targets
production bundling only.
- **RAM bundles** — not supported. Use Metro if you need indexed RAM bundles.
- **Flow types** — esbuild cannot strip Flow types natively. You'll need a Flow-
stripping Babel transform or a third-party esbuild plugin if your code uses
Flow.
63 changes: 63 additions & 0 deletions incubator/esbuild-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@rnx-kit/esbuild-service",
"version": "0.0.1",
"description": "EXPERIMENTAL - USE WITH CAUTION - esbuild-based bundler for React Native (Metro-independent)",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/esbuild-service#readme",
"license": "MIT",
"author": {
"name": "Microsoft Open Source",
"email": "microsoftopensource@users.noreply.github.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rnx-kit",
"directory": "incubator/esbuild-service"
},
"files": [
"lib/**/*.d.ts",
"lib/**/*.js"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./lib/index.d.ts",
"typescript": "./src/index.ts",
"default": "./lib/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"dependencies": {
"@rnx-kit/metro-service": "*",
"@rnx-kit/tools-node": "^3.0.4",
"@rnx-kit/tools-react-native": "^2.3.3",
"@rnx-kit/types-bundle-config": "^1.0.0",
"esbuild": "^0.27.1"
},
"devDependencies": {
"@rnx-kit/scripts": "*",
"@rnx-kit/tsconfig": "*",
"@types/node": "^24.0.0",
"metro": "^0.83.3",
"react-native": "^0.83.0"
},
"engines": {
"node": ">=18.12"
},
"experimental": true,
"peerDependencies": {
"metro": ">=0.72.0"
},
"peerDependenciesMeta": {
"metro": {
"optional": true
}
}
}
Loading