|
| 1 | +# @rnx-kit/tools-babel |
| 2 | + |
| 3 | +[](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml) |
| 4 | +[](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 | |
0 commit comments