Skip to content

Commit 1670dba

Browse files
authored
feat(tools-babel): add tools-babel package for Babel configuration and parsing support (#4087)
1 parent d58e4bf commit 1670dba

21 files changed

Lines changed: 4599 additions & 18 deletions

.changeset/ten-ways-fail.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

incubator/tools-babel/README.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
# @rnx-kit/tools-babel
2+
3+
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
4+
[![npm version](https://img.shields.io/npm/v/@rnx-kit/tools-babel)](https://www.npmjs.com/package/@rnx-kit/tools-babel)
5+
6+
Utilities for working with Babel during Metro bundling's transform stage.
7+
Handles loading Babel configs for React Native, parsing code to
8+
Babel-compatible ASTs using fast native parsers, and introspecting and
9+
manipulating Babel plugins.
10+
11+
## Motivation
12+
13+
Metro's transform stage runs Babel on every module in a React Native bundle.
14+
This package provides the building blocks for a custom Metro transformer that
15+
can:
16+
17+
- **Parse faster** by using OXC (a Rust-based parser) or Hermes instead of
18+
Babel's own parser, with automatic fallback
19+
- **Load configs once** by caching the base Babel config across files and only
20+
adding per-file settings (HMR, caller info, platform)
21+
- **Manage plugins** by inspecting, filtering, and wrapping plugins for
22+
performance tracing
23+
- **Trace performance** by integrating with `@rnx-kit/tools-performance` to
24+
measure parse, conversion, and per-plugin visitor times
25+
26+
## Installation
27+
28+
```sh
29+
yarn add @rnx-kit/tools-babel --dev
30+
```
31+
32+
or if you're using npm
33+
34+
```sh
35+
npm add --save-dev @rnx-kit/tools-babel
36+
```
37+
38+
Peer dependencies:
39+
40+
```sh
41+
yarn add @babel/core @react-native/babel-preset
42+
```
43+
44+
## Quick Start
45+
46+
The simplest integration builds `TransformerArgs` from Metro's input and parses
47+
with automatic backend selection:
48+
49+
```typescript
50+
import { makeTransformerArgs, parseToAst } from "@rnx-kit/tools-babel";
51+
52+
function transform({ filename, src, options, plugins }) {
53+
const args = makeTransformerArgs(
54+
{ filename, src, options, plugins },
55+
settings
56+
);
57+
if (!args) return null; // file should be skipped
58+
59+
const ast = parseToAst(args);
60+
// ast is a Babel-compatible AST ready for transformFromAstSync
61+
}
62+
```
63+
64+
## Parsing
65+
66+
Three parser backends are available. `parseToAst` tries them in order and
67+
returns the first successful result.
68+
69+
### OXC (primary)
70+
71+
OXC is a fast Rust-based JavaScript/TypeScript parser. Its output is an ESTree
72+
AST which `toBabelAST` converts to Babel's format in a single in-place pass.
73+
74+
```typescript
75+
import { oxcParseToAst } from "@rnx-kit/tools-babel";
76+
77+
const ast = oxcParseToAst(args);
78+
```
79+
80+
OXC is skipped automatically for files that may contain Flow syntax. Disable it
81+
explicitly via `TransformerSettings.parseDisableOxc`.
82+
83+
### Hermes (secondary)
84+
85+
Meta's Hermes parser, used as a fallback when OXC cannot parse a file.
86+
87+
```typescript
88+
import { hermesParseToAst } from "@rnx-kit/tools-babel";
89+
90+
const ast = hermesParseToAst(args);
91+
```
92+
93+
### Babel (fallback)
94+
95+
Babel's own `parseSync` is used as the final fallback. It is the slowest option
96+
but handles all syntax Babel supports.
97+
98+
### Fallback chain
99+
100+
```typescript
101+
import { parseToAst } from "@rnx-kit/tools-babel";
102+
103+
// Tries OXC -> Hermes -> Babel
104+
const ast = parseToAst(args);
105+
```
106+
107+
## Babel Config
108+
109+
### Loading configs
110+
111+
`getBabelConfig` creates a per-file Babel config by starting from a cached base
112+
config and layering on file-specific settings:
113+
114+
```typescript
115+
import { getBabelConfig } from "@rnx-kit/tools-babel";
116+
117+
const config = getBabelConfig(babelArgs, settings);
118+
// config is ready for transformFromAstSync(ast, src, config)
119+
```
120+
121+
The base config is resolved once and cached. It looks for `.babelrc`,
122+
`.babelrc.js`, or `babel.config.js` in the project root, falling back to
123+
`@react-native/babel-preset` if none is found.
124+
125+
Per-file additions include:
126+
127+
- HMR plugins (when `dev` + `hot` and not in `node_modules`)
128+
- Plugin visitor tracing (when high-frequency performance tracking is enabled)
129+
- Metro caller info with platform
130+
- `code: false, ast: true, sourceType: "unambiguous"`
131+
132+
### Filtering plugins
133+
134+
`filterConfigPlugins` resolves presets and overrides into a flat plugin list,
135+
then removes plugins by key:
136+
137+
```typescript
138+
import { filterConfigPlugins } from "@rnx-kit/tools-babel";
139+
140+
const disabled = new Set(["transform-flow-strip-types"]);
141+
const filtered = filterConfigPlugins(config, disabled);
142+
```
143+
144+
Returns `null` if the file should be skipped entirely.
145+
146+
## Transformer Context
147+
148+
`TransformerArgs` bundles everything needed for a transform pass: source,
149+
filename, Babel config, and a context object with file metadata and persistent
150+
settings.
151+
152+
### Building args
153+
154+
```typescript
155+
import { makeTransformerArgs } from "@rnx-kit/tools-babel";
156+
157+
const args = makeTransformerArgs(
158+
{ filename, src, options, plugins },
159+
settings,
160+
(context, babelArgs) => {
161+
// Optional: customize context before config is built
162+
context.configCallerMixins = { engine: "hermes" };
163+
}
164+
);
165+
```
166+
167+
### Initializing context only
168+
169+
If you need the file context without building the full Babel config:
170+
171+
```typescript
172+
import { initTransformerContext } from "@rnx-kit/tools-babel";
173+
174+
const context = initTransformerContext(filename, settings);
175+
// context.srcSyntax, context.mayContainFlow, context.isNodeModule, etc.
176+
```
177+
178+
## Plugin Utilities
179+
180+
Functions for inspecting and modifying Babel plugin configurations.
181+
182+
### Introspection
183+
184+
```typescript
185+
import {
186+
isConfigItem,
187+
isPluginObj,
188+
getPluginTarget,
189+
getPluginKey,
190+
} from "@rnx-kit/tools-babel";
191+
192+
// Identify plugin format
193+
isConfigItem(plugin); // true if ConfigItem (has `value` property)
194+
isPluginObj(plugin); // true if resolved PluginObj (has `visitor` property)
195+
196+
// Extract the plugin target (function/string) or key (string name)
197+
const target = getPluginTarget(plugin);
198+
const key = getPluginKey(plugin);
199+
```
200+
201+
### Modifying plugin chains
202+
203+
`updateTransformOptions` walks plugins, presets, and overrides, calling a
204+
visitor for each. Only creates new arrays/objects when changes are made.
205+
206+
```typescript
207+
import { updateTransformOptions } from "@rnx-kit/tools-babel";
208+
209+
const newConfig = updateTransformOptions(config, (plugin, isPreset) => {
210+
const key = getPluginKey(plugin);
211+
if (key === "transform-flow-strip-types") return null; // remove
212+
return plugin; // keep unchanged
213+
});
214+
```
215+
216+
## ESTree to Babel AST Conversion
217+
218+
`toBabelAST` converts an OXC ESTree `Program` into a Babel-compatible
219+
`ParseResult` in a single in-place pass. This is called automatically by
220+
`oxcParseToAst` but is available directly for advanced use cases.
221+
222+
```typescript
223+
import { toBabelAST } from "@rnx-kit/tools-babel";
224+
225+
const babelAst = toBabelAST(oxcProgram, source, isTypeScript, comments);
226+
```
227+
228+
The conversion handles:
229+
230+
- Node type renames (e.g. `Property` to `ObjectProperty`/`ObjectMethod`)
231+
- Literal splitting (`Literal` to `StringLiteral`, `NumericLiteral`, etc.)
232+
- Optional chaining (`ChainExpression` to `OptionalMemberExpression`/`OptionalCallExpression`)
233+
- Class member restructuring (`MethodDefinition` to `ClassMethod`/`ClassPrivateMethod`)
234+
- TypeScript-specific nodes (`TSFunctionType`, `TSInterfaceHeritage`, etc.)
235+
- Directive extraction from statement bodies
236+
- Import expression conversion to `CallExpression(Import)`
237+
- Comment attachment from OXC's flat array to Babel's per-node format
238+
- Top-level await detection
239+
- Source location calculation from byte offsets
240+
241+
## Performance Tracing
242+
243+
The package integrates with `@rnx-kit/tools-performance` on two domains:
244+
245+
| Domain | Frequency | What is traced |
246+
| -------------- | --------- | ---------------------------------------------------- |
247+
| `transform` | medium | Parse operations (OXC native, AST conversion, Babel) |
248+
| `babel-plugin` | high | Individual plugin visitor method calls |
249+
250+
Plugin visitor tracing wraps every visitor method via Babel's
251+
`wrapPluginVisitorMethod` hook. It is only enabled when high-frequency tracking
252+
is active for the `babel-plugin` domain, as it adds overhead to every visitor
253+
call.
254+
255+
```typescript
256+
import { trackPerformance } from "@rnx-kit/tools-performance";
257+
258+
// Enable transform-level tracing
259+
trackPerformance({ enable: "transform", strategy: "timing" });
260+
261+
// Enable per-plugin tracing (high overhead)
262+
trackPerformance({
263+
enable: "babel-plugin",
264+
strategy: "timing",
265+
frequency: "high",
266+
});
267+
```
268+
269+
## TransformerSettings
270+
271+
Settings that persist across transformation passes:
272+
273+
| Field | Type | Default | Description |
274+
| ----------------------- | --------------------------- | ------- | --------------------------------------------------------- |
275+
| `configCallerMixins` | `Record<string, string>` | -- | Extra fields added to Babel's `caller` config |
276+
| `configDisabledPlugins` | `Set<string>` | -- | Plugin keys to remove from the resolved config |
277+
| `parseDisableOxc` | `boolean` | -- | Disable OXC parser |
278+
| `parseDisableHermes` | `boolean` | -- | Disable Hermes parser |
279+
| `parseFlowDefault` | `boolean` | `true` | Assume Flow in `.js`/`.jsx` files under `node_modules` |
280+
| `parseFlowWorkspace` | `boolean` | `false` | Assume Flow in workspace `.js`/`.jsx` files |
281+
| `parseExtDefault` | `SrcSyntax` | `"js"` | Syntax for unknown file extensions (unset to skip) |
282+
| `parseExtAliases` | `Record<string, SrcSyntax>` | -- | Map extensions to syntax types (e.g. `{ ".svg": "jsx" }`) |
283+
284+
## API Reference
285+
286+
### Config
287+
288+
| Function | Description |
289+
| ---------------------------------------- | ------------------------------------------------------------------------------ |
290+
| `getBabelConfig(args, settings?)` | Build a per-file Babel config from cached base config + file-specific settings |
291+
| `filterConfigPlugins(config, disabled?)` | Resolve presets/overrides and filter plugins by key |
292+
293+
### Parsing
294+
295+
| Function | Description |
296+
| ------------------------------------------------------- | ------------------------------------------------- |
297+
| `parseToAst(args)` | Parse with fallback chain: OXC -> Hermes -> Babel |
298+
| `oxcParseToAst(args, trace?)` | Parse with OXC and convert ESTree to Babel AST |
299+
| `hermesParseToAst(args)` | Parse with Hermes |
300+
| `toBabelAST(program, source, isTypeScript?, comments?)` | Convert OXC ESTree to Babel AST |
301+
302+
### Transformer
303+
304+
| Function | Description |
305+
| ----------------------------------------------------------- | ----------------------------------------------------- |
306+
| `makeTransformerArgs(babelArgs, settings?, updateContext?)` | Build `TransformerArgs` with context and Babel config |
307+
| `initTransformerContext(filename, settings)` | Initialize file context without building Babel config |
308+
309+
### Plugins
310+
311+
| Function | Description |
312+
| ------------------------------------------ | ----------------------------------------------------- |
313+
| `isConfigItem(plugin)` | Check if plugin is a Babel `ConfigItem` |
314+
| `isPluginObj(plugin)` | Check if plugin is a resolved `PluginObj` |
315+
| `getPluginTarget(plugin)` | Extract the plugin target (function or string) |
316+
| `getPluginKey(plugin)` | Extract the key from a resolved plugin |
317+
| `updateTransformOptions(options, visitor)` | Walk and modify plugins/presets/overrides in a config |

incubator/tools-babel/package.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "@rnx-kit/tools-babel",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "EXPERIMENTAL - USE WITH CAUTION - tools-babel",
6+
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/tools-babel#readme",
7+
"license": "MIT",
8+
"author": {
9+
"name": "Microsoft Open Source",
10+
"email": "microsoftopensource@users.noreply.github.com"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/microsoft/rnx-kit",
15+
"directory": "incubator/tools-babel"
16+
},
17+
"files": [
18+
"lib/**/*.d.ts",
19+
"lib/**/*.js"
20+
],
21+
"type": "commonjs",
22+
"main": "lib/index.js",
23+
"types": "lib/index.d.ts",
24+
"scripts": {
25+
"build": "rnx-kit-scripts build",
26+
"format": "rnx-kit-scripts format",
27+
"lint": "rnx-kit-scripts lint",
28+
"test": "rnx-kit-scripts test"
29+
},
30+
"dependencies": {
31+
"@rnx-kit/tools-performance": "^0.1.0",
32+
"hermes-parser": "^0.34.0",
33+
"oxc-parser": "^0.123.0"
34+
},
35+
"devDependencies": {
36+
"@babel/core": "^7.20.0",
37+
"@babel/generator": "^7.20.0",
38+
"@react-native/babel-preset": "^0.83.0",
39+
"@rnx-kit/reporter": "*",
40+
"@rnx-kit/scripts": "*",
41+
"@rnx-kit/test-fixtures": "*",
42+
"@rnx-kit/tsconfig": "*",
43+
"@swc/core": "^1.15.24",
44+
"@types/babel__core": "^7.20.0",
45+
"@types/babel__generator": "^7.20.0",
46+
"metro-babel-transformer": "^0.83.1"
47+
},
48+
"peerDependencies": {
49+
"@babel/core": "*",
50+
"@react-native/babel-preset": "*"
51+
},
52+
"engines": {
53+
"node": ">=22.11"
54+
},
55+
"experimental": true
56+
}

0 commit comments

Comments
 (0)