Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions .changeset/ten-ways-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
317 changes: 317 additions & 0 deletions incubator/tools-babel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# @rnx-kit/tools-babel

[![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/tools-babel)](https://www.npmjs.com/package/@rnx-kit/tools-babel)

Utilities for working with Babel during Metro bundling's transform stage.
Handles loading Babel configs for React Native, parsing code to
Babel-compatible ASTs using fast native parsers, and introspecting and
manipulating Babel plugins.

## Motivation

Metro's transform stage runs Babel on every module in a React Native bundle.
This package provides the building blocks for a custom Metro transformer that
can:

- **Parse faster** by using OXC (a Rust-based parser) or Hermes instead of
Babel's own parser, with automatic fallback
- **Load configs once** by caching the base Babel config across files and only
adding per-file settings (HMR, caller info, platform)
- **Manage plugins** by inspecting, filtering, and wrapping plugins for
performance tracing
- **Trace performance** by integrating with `@rnx-kit/tools-performance` to
measure parse, conversion, and per-plugin visitor times

## Installation

```sh
yarn add @rnx-kit/tools-babel --dev
```

or if you're using npm

```sh
npm add --save-dev @rnx-kit/tools-babel
```

Peer dependencies:

```sh
yarn add @babel/core @react-native/babel-preset
```

## Quick Start

The simplest integration builds `TransformerArgs` from Metro's input and parses
with automatic backend selection:

```typescript
import { makeTransformerArgs, parseToAst } from "@rnx-kit/tools-babel";

function transform({ filename, src, options, plugins }) {
const args = makeTransformerArgs(
{ filename, src, options, plugins },
settings
);
if (!args) return null; // file should be skipped

const ast = parseToAst(args);
// ast is a Babel-compatible AST ready for transformFromAstSync
}
```

## Parsing

Three parser backends are available. `parseToAst` tries them in order and
returns the first successful result.

### OXC (primary)

OXC is a fast Rust-based JavaScript/TypeScript parser. Its output is an ESTree
AST which `toBabelAST` converts to Babel's format in a single in-place pass.

```typescript
import { oxcParseToAst } from "@rnx-kit/tools-babel";

const ast = oxcParseToAst(args);
```

OXC is skipped automatically for files that may contain Flow syntax. Disable it
explicitly via `TransformerSettings.parseDisableOxc`.

### Hermes (secondary)

Meta's Hermes parser, used as a fallback when OXC cannot parse a file.

```typescript
import { hermesParseToAst } from "@rnx-kit/tools-babel";

const ast = hermesParseToAst(args);
```

### Babel (fallback)

Babel's own `parseSync` is used as the final fallback. It is the slowest option
but handles all syntax Babel supports.

### Fallback chain

```typescript
import { parseToAst } from "@rnx-kit/tools-babel";

// Tries OXC -> Hermes -> Babel
const ast = parseToAst(args);
```

## Babel Config

### Loading configs

`getBabelConfig` creates a per-file Babel config by starting from a cached base
config and layering on file-specific settings:

```typescript
import { getBabelConfig } from "@rnx-kit/tools-babel";

const config = getBabelConfig(babelArgs, settings);
// config is ready for transformFromAstSync(ast, src, config)
```

The base config is resolved once and cached. It looks for `.babelrc`,
`.babelrc.js`, or `babel.config.js` in the project root, falling back to
`@react-native/babel-preset` if none is found.

Per-file additions include:

- HMR plugins (when `dev` + `hot` and not in `node_modules`)
- Plugin visitor tracing (when high-frequency performance tracking is enabled)
- Metro caller info with platform
- `code: false, ast: true, sourceType: "unambiguous"`

### Filtering plugins

`filterConfigPlugins` resolves presets and overrides into a flat plugin list,
then removes plugins by key:

```typescript
import { filterConfigPlugins } from "@rnx-kit/tools-babel";

const disabled = new Set(["transform-flow-strip-types"]);
const filtered = filterConfigPlugins(config, disabled);
```

Returns `null` if the file should be skipped entirely.

## Transformer Context

`TransformerArgs` bundles everything needed for a transform pass: source,
filename, Babel config, and a context object with file metadata and persistent
settings.

### Building args

```typescript
import { makeTransformerArgs } from "@rnx-kit/tools-babel";

const args = makeTransformerArgs(
{ filename, src, options, plugins },
settings,
(context, babelArgs) => {
// Optional: customize context before config is built
context.configCallerMixins = { engine: "hermes" };
}
);
```

### Initializing context only

If you need the file context without building the full Babel config:

```typescript
import { initTransformerContext } from "@rnx-kit/tools-babel";

const context = initTransformerContext(filename, settings);
// context.srcSyntax, context.mayContainFlow, context.isNodeModule, etc.
```

## Plugin Utilities

Functions for inspecting and modifying Babel plugin configurations.

### Introspection

```typescript
import {
isConfigItem,
isPluginObj,
getPluginTarget,
getPluginKey,
} from "@rnx-kit/tools-babel";

// Identify plugin format
isConfigItem(plugin); // true if ConfigItem (has `value` property)
isPluginObj(plugin); // true if resolved PluginObj (has `visitor` property)

// Extract the plugin target (function/string) or key (string name)
const target = getPluginTarget(plugin);
const key = getPluginKey(plugin);
```

### Modifying plugin chains

`updateTransformOptions` walks plugins, presets, and overrides, calling a
visitor for each. Only creates new arrays/objects when changes are made.

```typescript
import { updateTransformOptions } from "@rnx-kit/tools-babel";

const newConfig = updateTransformOptions(config, (plugin, isPreset) => {
const key = getPluginKey(plugin);
if (key === "transform-flow-strip-types") return null; // remove
return plugin; // keep unchanged
});
```

## ESTree to Babel AST Conversion

`toBabelAST` converts an OXC ESTree `Program` into a Babel-compatible
`ParseResult` in a single in-place pass. This is called automatically by
`oxcParseToAst` but is available directly for advanced use cases.

```typescript
import { toBabelAST } from "@rnx-kit/tools-babel";

const babelAst = toBabelAST(oxcProgram, source, isTypeScript, comments);
```

The conversion handles:

- Node type renames (e.g. `Property` to `ObjectProperty`/`ObjectMethod`)
- Literal splitting (`Literal` to `StringLiteral`, `NumericLiteral`, etc.)
- Optional chaining (`ChainExpression` to `OptionalMemberExpression`/`OptionalCallExpression`)
- Class member restructuring (`MethodDefinition` to `ClassMethod`/`ClassPrivateMethod`)
- TypeScript-specific nodes (`TSFunctionType`, `TSInterfaceHeritage`, etc.)
- Directive extraction from statement bodies
- Import expression conversion to `CallExpression(Import)`
- Comment attachment from OXC's flat array to Babel's per-node format
- Top-level await detection
- Source location calculation from byte offsets

## Performance Tracing

The package integrates with `@rnx-kit/tools-performance` on two domains:

| Domain | Frequency | What is traced |
| -------------- | --------- | ---------------------------------------------------- |
| `transform` | medium | Parse operations (OXC native, AST conversion, Babel) |
| `babel-plugin` | high | Individual plugin visitor method calls |

Plugin visitor tracing wraps every visitor method via Babel's
`wrapPluginVisitorMethod` hook. It is only enabled when high-frequency tracking
is active for the `babel-plugin` domain, as it adds overhead to every visitor
call.

```typescript
import { trackPerformance } from "@rnx-kit/tools-performance";

// Enable transform-level tracing
trackPerformance({ enable: "transform", strategy: "timing" });

// Enable per-plugin tracing (high overhead)
trackPerformance({
enable: "babel-plugin",
strategy: "timing",
frequency: "high",
});
```

## TransformerSettings

Settings that persist across transformation passes:

| Field | Type | Default | Description |
| ----------------------- | --------------------------- | ------- | --------------------------------------------------------- |
| `configCallerMixins` | `Record<string, string>` | -- | Extra fields added to Babel's `caller` config |
| `configDisabledPlugins` | `Set<string>` | -- | Plugin keys to remove from the resolved config |
| `parseDisableOxc` | `boolean` | -- | Disable OXC parser |
| `parseDisableHermes` | `boolean` | -- | Disable Hermes parser |
| `parseFlowDefault` | `boolean` | `true` | Assume Flow in `.js`/`.jsx` files under `node_modules` |
| `parseFlowWorkspace` | `boolean` | `false` | Assume Flow in workspace `.js`/`.jsx` files |
| `parseExtDefault` | `SrcSyntax` | `"js"` | Syntax for unknown file extensions (unset to skip) |
| `parseExtAliases` | `Record<string, SrcSyntax>` | -- | Map extensions to syntax types (e.g. `{ ".svg": "jsx" }`) |

## API Reference

### Config

| Function | Description |
| ---------------------------------------- | ------------------------------------------------------------------------------ |
| `getBabelConfig(args, settings?)` | Build a per-file Babel config from cached base config + file-specific settings |
| `filterConfigPlugins(config, disabled?)` | Resolve presets/overrides and filter plugins by key |

### Parsing

| Function | Description |
| ------------------------------------------------------- | ------------------------------------------------- |
| `parseToAst(args)` | Parse with fallback chain: OXC -> Hermes -> Babel |
| `oxcParseToAst(args, trace?)` | Parse with OXC and convert ESTree to Babel AST |
| `hermesParseToAst(args)` | Parse with Hermes |
| `toBabelAST(program, source, isTypeScript?, comments?)` | Convert OXC ESTree to Babel AST |

### Transformer

| Function | Description |
| ----------------------------------------------------------- | ----------------------------------------------------- |
| `makeTransformerArgs(babelArgs, settings?, updateContext?)` | Build `TransformerArgs` with context and Babel config |
| `initTransformerContext(filename, settings)` | Initialize file context without building Babel config |

### Plugins

| Function | Description |
| ------------------------------------------ | ----------------------------------------------------- |
| `isConfigItem(plugin)` | Check if plugin is a Babel `ConfigItem` |
| `isPluginObj(plugin)` | Check if plugin is a resolved `PluginObj` |
| `getPluginTarget(plugin)` | Extract the plugin target (function or string) |
| `getPluginKey(plugin)` | Extract the key from a resolved plugin |
| `updateTransformOptions(options, visitor)` | Walk and modify plugins/presets/overrides in a config |
56 changes: 56 additions & 0 deletions incubator/tools-babel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@rnx-kit/tools-babel",
"version": "0.0.1",
"private": true,
"description": "EXPERIMENTAL - USE WITH CAUTION - tools-babel",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/tools-babel#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/tools-babel"
},
"files": [
"lib/**/*.d.ts",
"lib/**/*.js"
],
"type": "commonjs",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"test": "rnx-kit-scripts test"
},
"dependencies": {
"@rnx-kit/tools-performance": "^0.1.0",
"hermes-parser": "^0.34.0",
"oxc-parser": "^0.123.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/generator": "^7.20.0",
"@react-native/babel-preset": "^0.83.0",
"@rnx-kit/reporter": "*",
"@rnx-kit/scripts": "*",
"@rnx-kit/test-fixtures": "*",
"@rnx-kit/tsconfig": "*",
"@swc/core": "^1.15.24",
"@types/babel__core": "^7.20.0",
"@types/babel__generator": "^7.20.0",
"metro-babel-transformer": "^0.83.1"
},
"peerDependencies": {
"@babel/core": "*",
"@react-native/babel-preset": "*"
},
"engines": {
"node": ">=22.11"
},
"experimental": true
}
Loading
Loading