diff --git a/.changeset/ten-ways-fail.md b/.changeset/ten-ways-fail.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/ten-ways-fail.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/incubator/tools-babel/README.md b/incubator/tools-babel/README.md new file mode 100644 index 000000000..805eff975 --- /dev/null +++ b/incubator/tools-babel/README.md @@ -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` | -- | Extra fields added to Babel's `caller` config | +| `configDisabledPlugins` | `Set` | -- | 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` | -- | 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 | diff --git a/incubator/tools-babel/package.json b/incubator/tools-babel/package.json new file mode 100644 index 000000000..af96eac35 --- /dev/null +++ b/incubator/tools-babel/package.json @@ -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 +} diff --git a/incubator/tools-babel/src/config.ts b/incubator/tools-babel/src/config.ts new file mode 100644 index 000000000..e0276e4d4 --- /dev/null +++ b/incubator/tools-babel/src/config.ts @@ -0,0 +1,164 @@ +import type { + PluginItem, + TransformCaller, + TransformOptions, +} from "@babel/core"; +import { loadOptions, loadPartialConfig } from "@babel/core"; +import { lazyInit } from "@rnx-kit/reporter"; +import fs from "node:fs"; +import path from "node:path"; +import { + getPluginKey, + pluginTraceFactory, + shouldInstrumentPlugins, +} from "./plugins.ts"; +import type { BabelTransformerArgs, TransformerSettings } from "./types.ts"; +import type { BabelTransformerOptions } from "./types.ts"; + +// cache the hot module reload config the first time it is requested. +const getHmrConfig = lazyInit(() => + require("@react-native/babel-preset/src/configs/hmr")() +); + +function checkRcFile(projectRoot: string, file: string): string | undefined { + const filePath = path.resolve(projectRoot, file); + return fs.existsSync(filePath) ? filePath : undefined; +} + +function assertValue(value: T | null | undefined, message?: string): T { + if (value == null) { + throw new Error(message ?? "Unexpected null or undefined value"); + } + return value; +} + +/** + * Create the base babel config for the transformer. + */ +const getBabelRC = (function () { + let babelRC: TransformOptions | null = null; + + return function _getBabelRC({ + projectRoot, + extendsBabelConfigPath, + experimentalImportSupport, + ...options + }: BabelTransformerOptions) { + if (babelRC != null) { + return babelRC; + } + + const rcExtends = + extendsBabelConfigPath ?? + checkRcFile(projectRoot, ".babelrc") ?? + checkRcFile(projectRoot, ".babelrc.js") ?? + checkRcFile(projectRoot, "babel.config.js"); + + return (babelRC = assertValue( + loadPartialConfig({ + extends: rcExtends, + cwd: projectRoot, + plugins: [], + ...(rcExtends == null && { + presets: [ + [ + require("@react-native/babel-preset"), + { + projectRoot, + ...options, + disableImportExportTransform: experimentalImportSupport, + }, + ], + ], + }), + })?.options, + "Failed to load a Babel config. Please ensure you have a valid Babel config file or that @react-native/babel-preset is installed." + )); + }; +})(); + +/** + * Optionally filters the set of plugins in the config based on the provided disabled plugin keys. To do this the config is fully + * loaded which will resolve all presets and overrides into a flattened plugins array. This allows matching on key rather than comparing + * resolution results. + * @param config babel TransformOptions to operate on + * @param disabledPlugins a set of plugin keys to disable + * @returns either the original config, or a filtered config if disabled plugins were found, or null if the file should be skipped + */ +export function filterConfigPlugins( + config: TransformOptions, + disabledPlugins?: Set +): TransformOptions | null { + if (!disabledPlugins || disabledPlugins.size === 0) { + return config; + } + + // to disable the plugins, we need to load the options which will resolve presets & overrides and give the full plugin list + const newConfig: TransformOptions | null = loadOptions(config); + if (newConfig && newConfig.plugins && newConfig.plugins.length > 0) { + newConfig.plugins = newConfig.plugins.filter((plugin: PluginItem) => { + const key = getPluginKey(plugin); + return key == null || !disabledPlugins.has(key); + }); + } + return newConfig; +} + +/** + * Get the babel config for a specific file. This caches as much of the config as possible + * across files, only adding file-specific settings (filename, HMR plugins) per call. + * @param args the transformer args, including file specific options + * @returns the babel config for the file or null if the file should be skipped + */ +export function getBabelConfig( + args: BabelTransformerArgs, + settings: Partial = {} +): TransformOptions | null { + const { filename, options, plugins: argPlugins } = args; + const { configCallerMixins } = settings; + const { enableBabelRCLookup = true, dev, hot } = options; + + // make a copy of the cached config, duplicating the plugins array as well so it can be modified + const babelRc = { ...getBabelRC(options) }; + const plugins = (babelRc.plugins = babelRc.plugins + ? [...babelRc.plugins] + : []); + + // setup hot module reload if required, mixin the config and append any plugins + if (dev && hot && !filename.includes("node_modules")) { + const { plugins: hmrPlugins, ...hmrConfig } = getHmrConfig(); + Object.assign(babelRc, hmrConfig); + if (hmrPlugins && hmrPlugins.length > 0) { + plugins.push(...hmrPlugins); + } + } + + // conditionally enable plugin performance tracing, only enabled if requested as it adds overhead to every visitor call + const wrapPluginVisitorMethod = shouldInstrumentPlugins() + ? pluginTraceFactory() + : undefined; + + // add the parameter plugins if present (typically there are two fixed ones metro adds) + if (argPlugins && argPlugins.length > 0) { + plugins.push(...argPlugins); + } + + // now configure the full config for this file + return Object.assign(babelRc, { + filename, + caller: { + name: "metro", + bundler: "metro", // custom metro key + platform: options.platform, + ...configCallerMixins, // custom mixins for caller + } as TransformCaller, + babelrc: enableBabelRCLookup, + envName: dev ? "development" : process.env.BABEL_ENV || "production", + code: false, + highlightCode: true, + ast: true, + cloneInputAst: false, + sourceType: "unambiguous", + wrapPluginVisitorMethod, + }); +} diff --git a/incubator/tools-babel/src/estree.ts b/incubator/tools-babel/src/estree.ts new file mode 100644 index 000000000..d67b6f219 --- /dev/null +++ b/incubator/tools-babel/src/estree.ts @@ -0,0 +1,1633 @@ +import type { Node as BabelNode, ParseResult } from "@babel/core"; +import type { Node, Program, VisitorObject } from "oxc-parser" with { + "resolution-mode": "import", +}; +import { Visitor, visitorKeys } from "oxc-parser"; + +// ── Source location calculation ────────────────────────────────────── + +type SourceLocation = NonNullable; + +// Loosely typed node for mutation during conversion. We intentionally change +// `type` from ESTree values to Babel values and add/remove arbitrary properties. +// oxlint-disable-next-line typescript/no-explicit-any +type MutableNode = Record & { + loc?: SourceLocation; + type: string; + start: number; + end: number; +}; + +/** + * Binary search for the line number containing a byte offset. + * Returns the 1-based line number. + */ +function getLine(offset: number, newlines: number[]): number { + let left = 0; + let right = newlines.length; + while (left < right) { + const mid = (left + right) >> 1; + if (newlines[mid] < offset) { + left = mid + 1; + } else { + right = mid; + } + } + return left + 1; +} + +/** + * Compute the column for a byte offset given its 1-based line number. + */ +function getColumn(offset: number, line: number, newlines: number[]): number { + return line <= 1 ? offset : offset - newlines[line - 2] - 1; +} + +// Minimal loc shape that satisfies Babel without allocating unused fields. +// `filename` and `identifierName` are typed as required by SourceLocation +// but Babel doesn't need them on every node. +type MinimalLoc = { + start: { line: number; column: number; index: number }; + end: { line: number; column: number; index: number }; + identifierName?: string; +}; + +function setLocation(node: MutableNode, newlines: number[]): void { + const startLine = getLine(node.start, newlines); + const endLine = getLine(node.end, newlines); + const loc: MinimalLoc = { + start: { + line: startLine, + column: getColumn(node.start, startLine, newlines), + index: node.start, + }, + end: { + line: endLine, + column: getColumn(node.end, endLine, newlines), + index: node.end, + }, + }; + if (node.type === "Identifier") { + loc.identifierName = node.name; + } + node.loc = loc as SourceLocation; +} + +export function getNewlines(src: string): number[] { + const newlines: number[] = []; + let i = src.indexOf("\n"); + while (i >= 0) { + newlines.push(i); + i = src.indexOf("\n", i + 1); + } + return newlines; +} + +// ── Comment conversion ────────────────────────────────────────────── + +function convertComment(comment: MutableNode): void { + if (comment.type === "Line") { + comment.type = "CommentLine"; + } else if (comment.type === "Block") { + comment.type = "CommentBlock"; + } +} + +/** + * Distribute node-attached comments into Babel's leading/trailing/inner format. + */ +function convertNodeComments(node: MutableNode): void { + const comments = node.comments; + if (!comments) return; + + delete node.comments; + let leading: MutableNode[] | undefined; + let trailing: MutableNode[] | undefined; + let inner: MutableNode[] | undefined; + + for (const comment of comments) { + convertComment(comment); + if (comment.trailing) { + (trailing ??= []).push(comment); + } else if (comment.leading) { + (leading ??= []).push(comment); + } else { + (inner ??= []).push(comment); + } + delete comment.leading; + delete comment.trailing; + } + + if (leading) node.leadingComments = leading; + if (trailing) node.trailingComments = trailing; + if (inner) node.innerComments = inner; +} + +// ── Imperative conversion functions ───────────────────────────────── +// These handle complex structural transforms that don't fit a table. + +function convertLiteral(node: MutableNode): void { + if (node.type !== "Literal") return; + + const { value, raw } = node; + + if (raw !== undefined) { + node.extra = node.extra || {}; + node.extra.raw = raw; + node.extra.rawValue = value; + delete node.raw; + } + + if (value === null) { + node.type = "NullLiteral"; + delete node.extra; + } else if (typeof value === "string") { + node.type = "StringLiteral"; + } else if (typeof value === "number") { + node.type = "NumericLiteral"; + } else if (typeof value === "boolean") { + node.type = "BooleanLiteral"; + delete node.extra; + } else if (node.bigint != null) { + node.type = "BigIntLiteral"; + const bigintStr = + typeof node.bigint === "string" ? node.bigint : String(node.bigint); + node.value = bigintStr; + delete node.bigint; + if (node.extra) { + node.extra.rawValue = bigintStr; + } + } else if (node.regex) { + node.type = "RegExpLiteral"; + node.pattern = node.regex.pattern; + node.flags = node.regex.flags; + delete node.regex; + delete node.value; + if (node.extra) { + delete node.extra.rawValue; + } + } +} + +/** + * Collect chain nodes in bottom-up order, then mark them top-down. + * Single O(N) pass instead of O(N²) from nested hasOptionalDescendant checks. + */ +function markOptionalChain(root: MutableNode): void { + if (!root) return; + + const chain: MutableNode[] = []; + let node: MutableNode | undefined = root; + let lowestOptional = -1; + + while (node) { + if (node.type === "MemberExpression" || node.type === "CallExpression") { + chain.push(node); + if (node.optional) lowestOptional = chain.length - 1; + node = node.type === "MemberExpression" ? node.object : node.callee; + } else { + break; + } + } + + for (let i = 0; i <= lowestOptional; i++) { + const n = chain[i]; + if (n.type === "MemberExpression") { + n.type = "OptionalMemberExpression"; + } else { + n.type = "OptionalCallExpression"; + } + if (!n.optional) n.optional = false; + } +} + +function convertChainExpression(node: MutableNode): void { + const expr = node.expression; + if (!expr) return; + markOptionalChain(expr); + inlineReplaceWithExpression(node); +} + +function convertMethodDefinition(node: MutableNode): void { + const { key, value } = node; + if (!value) return; + + const keyType = key?.type; + const isPrivate = + keyType === "PrivateIdentifier" || keyType === "PrivateName"; + + node.params = value.params; + node.body = value.body; + node.generator = value.generator; + node.async = value.async; + node.id = value.id ?? null; + delete node.value; + delete node.expression; + + if (isPrivate) { + node.type = "ClassPrivateMethod"; + if (keyType === "PrivateIdentifier") convertPrivateIdentifierToName(key); + if (node.kind === "get" || node.kind === "set") { + if (node.computed == null) node.computed = false; + } else { + delete node.computed; + } + } else if (node.body === null) { + node.type = "TSDeclareMethod"; + if (node.computed == null) node.computed = false; + } else { + node.type = "ClassMethod"; + if (node.computed == null) node.computed = false; + } +} + +function convertPropertyDefinition(node: MutableNode): void { + const keyType = node.key && node.key.type; + if (keyType === "PrivateIdentifier" || keyType === "PrivateName") { + node.type = "ClassPrivateProperty"; + if (keyType === "PrivateIdentifier") + convertPrivateIdentifierToName(node.key); + delete node.computed; + } else { + node.type = "ClassProperty"; + if (node.computed == null) node.computed = false; + } +} + +function convertTSAbstractPropertyDefinition(node: MutableNode): void { + node.type = "ClassProperty"; + node.abstract = true; + if (node.computed == null) node.computed = false; +} + +function convertPrivateIdentifierToName(node: MutableNode): void { + const name = node.name; + const loc = node.loc; + node.type = "PrivateName"; + node.id = { + type: "Identifier", + name, + loc: loc + ? { + start: { + line: loc.start.line, + column: loc.start.column + 1, + }, + end: loc.end, + } + : undefined, + }; + delete node.name; +} + +function markParenthesized(node: MutableNode): void { + const expr = node.expression; + if (!expr) return; + expr.extra = expr.extra || {}; + expr.extra.parenthesized = true; + expr.extra.parenStart = node.start; +} + +function inlineReplaceWithExpression(node: MutableNode): void { + const expr = node.expression; + if (!expr) return; + // Check if the inner node has its own `expression` field (e.g. TSAsExpression, + // TSSatisfiesExpression). If so, we must NOT delete it after copying. + const innerHasExpression = "expression" in expr; + for (const key in expr) { + node[key] = expr[key]; + } + if (!innerHasExpression) { + delete node.expression; + } +} + +function extractDirectives(node: MutableNode): void { + if (node.directives) return; + + node.directives = []; + const body = node.body; + if (!Array.isArray(body)) return; + + let i = 0; + while (i < body.length) { + const stmt = body[i]; + if (stmt.type !== "ExpressionStatement" || !stmt.directive) break; + node.directives.push({ + type: "Directive", + value: { + type: "DirectiveLiteral", + value: stmt.directive, + extra: { + raw: JSON.stringify(stmt.directive), + rawValue: stmt.directive, + expressionValue: stmt.directive, + }, + }, + }); + i++; + } + + if (i > 0) { + node.body = body.slice(i); + } +} + +function convertProperty(node: MutableNode): void { + const isObjectMethod = + node.value?.type === "FunctionExpression" && + (node.method || node.kind === "get" || node.kind === "set"); + + if (isObjectMethod) { + const value = node.value; + node.type = "ObjectMethod"; + if (node.method || node.kind === "init") { + node.kind = "method"; + } + node.params = value.params; + node.body = value.body; + node.generator = value.generator; + node.async = value.async; + node.id = null; + delete node.value; + delete node.shorthand; + } else { + node.type = "ObjectProperty"; + if (node.kind === "init") delete node.kind; + if (node.shorthand) { + node.extra = node.extra || {}; + node.extra.shorthand = true; + } + } +} + +function normalizeSpecifierImportKind(node: MutableNode): void { + if (ctx.isTypeScript) { + if (!node.importKind) node.importKind = "value"; + } else { + if (node.importKind === "value") delete node.importKind; + } +} + +function convertImportDeclaration(node: MutableNode): void { + if (!node.attributes) node.attributes = []; + if (!node.importKind) node.importKind = "value"; + // Babel omits importKind on bare side-effect imports (no specifiers) for JS files + if ( + !ctx.isTypeScript && + node.importKind === "value" && + (!node.specifiers || node.specifiers.length === 0) + ) { + delete node.importKind; + } +} + +function convertExportDeclaration(node: MutableNode): void { + if (!node.attributes) node.attributes = []; + if (!node.exportKind) node.exportKind = "value"; +} + +function convertExportAllWithExported(node: MutableNode): void { + if (!node.exported) return; + + const exported = node.exported; + if (exported.type === "Literal" && typeof exported.value === "string") { + exported.type = "StringLiteral"; + if (exported.raw !== undefined) { + exported.extra = exported.extra || {}; + exported.extra.raw = exported.raw; + exported.extra.rawValue = exported.value; + } + } + + node.type = "ExportNamedDeclaration"; + node.specifiers = [ + { + type: "ExportNamespaceSpecifier", + exported, + loc: exported.loc, + start: exported.start, + end: exported.end, + }, + ]; + node.declaration = null; + delete node.exported; +} + +function convertTSInterfaceHeritage(node: MutableNode): void { + let expr = node.expression; + while (expr?.type === "MemberExpression") { + expr.type = "TSQualifiedName"; + expr.left = expr.object; + expr.right = expr.property; + delete expr.object; + delete expr.property; + expr = expr.left; + } +} + +function convertTSAbstractMethodDefinition(node: MutableNode): void { + const { value } = node; + if (!value) return; + node.type = "TSDeclareMethod"; + node.abstract = true; + node.generator = value.generator; + node.async = value.async; + node.params = value.params; + node.id = value.id; + node.returnType = value.returnType; + delete node.value; +} + +function convertImportExpression(node: MutableNode): void { + const args = [node.source]; + if (node.options) args.push(node.options); + + node.type = "CallExpression"; + node.callee = { + type: "Import", + start: node.start, + end: node.start + 6, + loc: node.loc + ? { + start: node.loc.start, + end: { + line: node.loc.start.line, + column: node.loc.start.column + 6, + index: node.start + 6, + }, + } + : undefined, + }; + node.arguments = args; + delete node.source; + delete node.options; + delete node.phase; +} + +function convertEnumDeclaration(node: MutableNode): void { + if (node.body?.members && !node.members) { + node.members = node.body.members; + delete node.body; + } +} + +// ── Universal cleanup (hot path — runs on every node) ─────────────── + +/** + * Strip fields that OXC adds on many node types but Babel omits. + * Only truly universal checks belong here — type-specific cleanup + * is handled by the per-node-type ops table below. + */ +function cleanupOxcExtras(n: MutableNode): void { + if (n.decorators?.length === 0) delete n.decorators; + if (n.optional === false) delete n.optional; + if (n.typeAnnotation === null) delete n.typeAnnotation; + // These TS flags appear on many node types (PropertyDefinition, MethodDefinition, + // ClassDeclaration, FunctionExpression, VariableDeclarator, AccessorProperty, etc.) + // so they stay global rather than being enumerated per-type in the ops table. + if (n.declare === false) delete n.declare; + if (n.definite === false) delete n.definite; + if (n.override === false) delete n.override; + if (n.readonly === false) delete n.readonly; + if (n.abstract === false) delete n.abstract; +} + +// ── Per-node-type ops table ───────────────────────────────────────── +// +// Declarative rules applied per node type. This is the single place to +// look up "what transforms apply to node type X?". Complex structural +// transforms use the `custom` callback; simple field operations use the +// other fields. +// +// Consumed once at module load to build visitor handler functions. + +type NodeOps = { + /** Rename the node type */ + type?: string; + /** Fields to unconditionally delete */ + delete?: string[]; + /** Fields to delete when they are empty arrays */ + deleteIfEmpty?: string[]; + /** Fields to delete when false */ + deleteIfFalse?: string[]; + /** Field renames: [from, to] pairs */ + rename?: [from: string, to: string][]; + /** Default values to set if the field is undefined */ + defaults?: [field: string, value: unknown][]; + /** Key in ctx.deferred — node is collected for post-processing */ + defer?: string; + /** Imperative handler for logic that doesn't fit the table */ + custom?: (n: MutableNode) => void; +}; + +function applyOps(n: MutableNode, ops: NodeOps): void { + if (ops.type) n.type = ops.type; + if (ops.delete) { + for (const f of ops.delete) delete n[f]; + } + if (ops.deleteIfEmpty) { + for (const f of ops.deleteIfEmpty) { + if (n[f]?.length === 0) delete n[f]; + } + } + if (ops.deleteIfFalse) { + for (const f of ops.deleteIfFalse) { + if (n[f] === false) delete n[f]; + } + } + if (ops.rename) { + for (const [from, to] of ops.rename) { + if (n[from] !== undefined && n[to] === undefined) { + n[to] = n[from]; + delete n[from]; + } + } + } + if (ops.defaults) { + for (const [f, v] of ops.defaults) { + if (n[f] === undefined) n[f] = v; + } + } + if (ops.defer) ctx.deferred[ops.defer].push(n); + if (ops.custom) ops.custom(n); +} + +const tsFunctionTypeRenames: [string, string][] = [ + ["params", "parameters"], + ["returnType", "typeAnnotation"], +]; + +// ── The table ─────────────────────────────────────────────────────── + +const nodeOps: Record = { + // ── Literals ── + Literal: { custom: convertLiteral }, + + // ── Functions: delete expression, track ranges for top-level await ── + FunctionDeclaration: { + delete: ["expression"], + custom: (n) => ctx.functionRanges.push(n.start, n.end), + }, + FunctionExpression: { + delete: ["expression"], + custom: (n) => ctx.functionRanges.push(n.start, n.end), + }, + ArrowFunctionExpression: { + delete: ["expression"], + custom: (n) => ctx.functionRanges.push(n.start, n.end), + }, + TSDeclareFunction: { delete: ["expression"] }, + + // ── Top-level await detection ── + AwaitExpression: { + custom: (n) => { + if (!ctx.hasTopLevelAwait) { + ctx.hasTopLevelAwait = !isInsideFunction(n.start, n.end); + } + }, + }, + ForOfStatement: { + custom: (n) => { + if (!ctx.hasTopLevelAwait && n.await) { + ctx.hasTopLevelAwait = !isInsideFunction(n.start, n.end); + } + }, + }, + + // ── Properties ── + Property: { + custom: (n) => { + const isMethodLike = + n.value?.type === "FunctionExpression" && + (n.method || n.kind === "get" || n.kind === "set"); + if (isMethodLike) { + ctx.deferred.objectMethods.push(n); + } else { + n.type = "ObjectProperty"; + if (n.kind === "init") delete n.kind; + if (n.shorthand) { + n.extra = n.extra || {}; + n.extra.shorthand = true; + } + } + }, + }, + + // ── Import/Export specifiers ── + ImportSpecifier: { custom: normalizeSpecifierImportKind }, + ImportDefaultSpecifier: { custom: normalizeSpecifierImportKind }, + ImportNamespaceSpecifier: { custom: normalizeSpecifierImportKind }, + ExportSpecifier: { + custom: (n) => { + if (n.importKind === "value") delete n.importKind; + const exported = n.exported; + if (exported && exported.raw !== undefined) { + // raw hasn't been moved to extra yet (Literal → StringLiteral conversion is deferred) + if (exported.type === "Literal" || exported.type === "StringLiteral") { + delete exported.raw; + } + } + }, + }, + + // ── Import/Export declarations ── + ImportDeclaration: { custom: convertImportDeclaration }, + ExportNamedDeclaration: { custom: convertExportDeclaration }, + ExportDefaultDeclaration: { deleteIfEmpty: ["attributes"] }, + ExportAllDeclaration: { + custom: (n) => { + convertExportDeclaration(n); + convertExportAllWithExported(n); + }, + }, + + // ── Deferred nodes (need children visited first) ── + ImportExpression: { defer: "importExpressions" }, + ChainExpression: { defer: "chainExpressions" }, + MethodDefinition: { defer: "methods" }, + PropertyDefinition: { defer: "propertyDefs" }, + ParenthesizedExpression: { defer: "replacements", custom: markParenthesized }, + PrivateIdentifier: { custom: convertPrivateIdentifierToName }, + + // ── Program / Block directives ── + Program: { custom: extractDirectives }, + BlockStatement: { custom: extractDirectives }, + + // ── Classes ── + ClassDeclaration: { deleteIfEmpty: ["implements"] }, + ClassExpression: { deleteIfEmpty: ["implements"] }, + + // ── JSX ── + JSXOpeningFragment: { delete: ["attributes", "selfClosing"] }, + JSXText: { + custom: (n) => { + if (n.raw !== undefined) { + n.extra = n.extra || {}; + n.extra.raw = n.raw; + n.extra.rawValue = n.raw; + delete n.raw; + } + }, + }, + + // ── TypeScript: type renames ── + TSClassImplements: { type: "TSExpressionWithTypeArguments" }, + TSInterfaceHeritage: { + type: "TSExpressionWithTypeArguments", + custom: convertTSInterfaceHeritage, + }, + + // ── TypeScript: interface / type members ── + TSPropertySignature: { deleteIfFalse: ["static"] }, + TSIndexSignature: { deleteIfFalse: ["static"] }, + TSInterfaceDeclaration: { deleteIfEmpty: ["extends"] }, + + // ── TypeScript: function-like types (params→parameters, returnType→typeAnnotation) ── + TSFunctionType: { rename: tsFunctionTypeRenames }, + TSConstructorType: { rename: tsFunctionTypeRenames }, + TSCallSignatureDeclaration: { rename: tsFunctionTypeRenames }, + TSConstructSignatureDeclaration: { rename: tsFunctionTypeRenames }, + TSMethodSignature: { rename: tsFunctionTypeRenames }, + + // ── TypeScript: type parameters ── + TSTypeParameter: { + deleteIfFalse: ["const", "in", "out"], + custom: (n) => { + if ( + n.name && + typeof n.name === "object" && + n.name.type === "Identifier" + ) { + n.name = n.name.name; + } + }, + }, + + // ── TypeScript: enums ── + TSEnumMember: { delete: ["computed"] }, + TSEnumDeclaration: { defer: "enumDeclarations", deleteIfFalse: ["const"] }, + TSModuleDeclaration: { deleteIfFalse: ["global"] }, + + // ── TypeScript: import equals ── + TSImportEqualsDeclaration: { defaults: [["isExport", false]] }, + + // ── TypeScript: abstract members (deferred) ── + TSAbstractMethodDefinition: { defer: "abstractMethods" }, + TSAbstractPropertyDefinition: { defer: "abstractPropertyDefs" }, +}; + +// ── Post-processors for deferred nodes ────────────────────────────── +// Order matters: some conversions depend on children being fully visited. + +const postProcessors: [key: string, fn: (node: MutableNode) => void][] = [ + ["importExpressions", convertImportExpression], + ["chainExpressions", convertChainExpression], + ["replacements", inlineReplaceWithExpression], + ["methods", convertMethodDefinition], + ["propertyDefs", convertPropertyDefinition], + ["objectMethods", convertProperty], + ["abstractMethods", convertTSAbstractMethodDefinition], + ["enumDeclarations", convertEnumDeclaration], + ["abstractPropertyDefs", convertTSAbstractPropertyDefinition], +]; + +// Collect all deferred keys from the table + postProcessors +const deferredKeys = postProcessors.map(([key]) => key); + +// ── Conversion context ────────────────────────────────────────────── + +type ConvertContext = { + newlines: number[]; + hasTopLevelAwait: boolean; + isTypeScript: boolean; + /** Flat array of [start, end, start, end, ...] for all function nodes */ + functionRanges: number[]; + deferred: Record; +}; + +const ctx: ConvertContext = { + newlines: [], + hasTopLevelAwait: false, + isTypeScript: false, + functionRanges: [], + deferred: Object.fromEntries(deferredKeys.map((k) => [k, []])), +}; + +function resetContext(newlines: number[], isTypeScript: boolean): void { + ctx.newlines = newlines; + ctx.hasTopLevelAwait = false; + ctx.isTypeScript = isTypeScript; + ctx.functionRanges.length = 0; + for (const list of Object.values(ctx.deferred)) list.length = 0; +} + +/** + * Check if a node range is contained within any recorded function range. + */ +function isInsideFunction(start: number, end: number): boolean { + const ranges = ctx.functionRanges; + for (let i = 0; i < ranges.length; i += 2) { + if (start >= ranges[i] && end <= ranges[i + 1]) { + return true; + } + } + return false; +} + +// ── Visitor (built once at module load) ───────────────────────────── + +function buildVisitor(): VisitorObject { + const visitor: VisitorObject = {}; + + const processNode = (n: MutableNode) => { + setLocation(n, ctx.newlines); + cleanupOxcExtras(n); + }; + + // Default handler: location + cleanup + comments + const defaultHandler = (node: Node) => { + const n = node as MutableNode; + processNode(n); + if (n.comments) convertNodeComments(n); + }; + + for (const key of Object.keys(visitorKeys) as (keyof VisitorObject)[]) { + visitor[key] = defaultHandler; + } + + // Generate specialized handlers from the ops table + for (const [nodeType, ops] of Object.entries(nodeOps)) { + visitor[nodeType as keyof VisitorObject] = (node: Node) => { + const n = node as MutableNode; + processNode(n); + applyOps(n, ops); + if (n.comments) convertNodeComments(n); + }; + } + + return visitor; +} + +const moduleVisitor = buildVisitor(); + +// ── Post-visitor: rename typeArguments → typeParameters ────────────── +// Done after the visitor pass because the visitor uses visitorKeys which +// reference "typeArguments" — renaming during traversal breaks child visiting. + +/** + * Post-visitor tree walk that handles two tasks in a single pass: + * 1. Rename typeArguments → typeParameters, superTypeArguments → superTypeParameters + * 2. Fix comment attachment around function params/args using source text + */ +function postVisitorWalk(node: MutableNode, source: string | undefined): void { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const child of node) postVisitorWalk(child, source); + return; + } + if (node.typeArguments && !node.typeParameters) { + node.typeParameters = node.typeArguments; + delete node.typeArguments; + } + if (node.superTypeArguments) { + node.superTypeParameters = node.superTypeArguments; + delete node.superTypeArguments; + } + for (const key of Object.keys(node)) { + const val = node[key]; + if (val && typeof val === "object" && key !== "loc") { + postVisitorWalk(val, source); + } + } + if (source) { + fixupFunctionCommentsSingle(node, source); + fixupParenthesizedLeading(node); + } +} + +// ── Comment attachment ────────────────────────────────────────────── +// OXC returns comments as a flat array on the parse result, not attached +// to individual nodes. Babel's generator requires node-attached comments +// (leadingComments, trailingComments, innerComments). This section +// distributes comments to nodes based on source positions. + +type RawComment = { + type: string; + value: string; + start: number; + end: number; +}; + +// Fields to skip when collecting children for comment attachment +const COMMENT_SKIP_KEYS = new Set([ + "loc", + "type", + "start", + "end", + "leadingComments", + "trailingComments", + "innerComments", + "extra", + "directives", +]); + +function isAstNode(val: unknown): val is MutableNode { + return ( + val != null && + typeof val === "object" && + typeof (val as MutableNode).start === "number" && + typeof (val as MutableNode).end === "number" && + typeof (val as MutableNode).type === "string" + ); +} + +/** + * Find the lower bound index: first comment with start >= pos. + */ +function commentLowerBound(comments: MutableNode[], pos: number): number { + let lo = 0; + let hi = comments.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (comments[mid].start < pos) lo = mid + 1; + else hi = mid; + } + return lo; +} + +/** + * Attach comments to AST nodes based on source positions. + * + * Walks the tree field-by-field. For array fields (params, arguments, body, + * elements, etc.), comments between elements are attached as: + * - **body** arrays (statement lists): trailing on prev AND leading on next + * - **all other** arrays: ONLY leading on next + * - empty arrays: inner on the parent node + * + * Comments between different fields follow statement-body rules (both). + * Comments inside a leaf node with no children are inner. + * + * Returns the full converted comments array for File.comments. + */ +function attachComments( + root: MutableNode, + rawComments: RawComment[], + newlines: number[] +): MutableNode[] { + if (!rawComments || rawComments.length === 0) return []; + + // Convert raw OXC comments to Babel format with locations + const comments: MutableNode[] = rawComments.map((c) => { + const startLine = getLine(c.start, newlines); + const endLine = getLine(c.end, newlines); + return { + type: c.type === "Line" ? "CommentLine" : "CommentBlock", + value: c.value, + start: c.start, + end: c.end, + loc: { + start: { + line: startLine, + column: getColumn(c.start, startLine, newlines), + index: c.start, + }, + end: { + line: endLine, + column: getColumn(c.end, endLine, newlines), + index: c.end, + }, + } as SourceLocation, + }; + }); + + // Set of comment indices already attached — avoids double-attaching + const attached = new Set(); + + /** + * Claim unattached comments in [rangeStart, rangeEnd) and push to target array. + * Returns the index past the last comment checked. + */ + function claim( + ci: number, + rangeEnd: number, + target: MutableNode, + kind: "leadingComments" | "trailingComments" | "innerComments" + ): number { + while (ci < comments.length && comments[ci].end <= rangeEnd) { + if (!attached.has(ci)) { + (target[kind] ??= []).push(comments[ci]); + attached.add(ci); + } + ci++; + } + return ci; + } + + /** + * Like claim, but also attaches as trailing on `prev` (for body-style gaps). + */ + function claimBoth( + ci: number, + rangeEnd: number, + prev: MutableNode, + next: MutableNode + ): number { + while (ci < comments.length && comments[ci].end <= rangeEnd) { + if (!attached.has(ci)) { + (prev.trailingComments ??= []).push(comments[ci]); + (next.leadingComments ??= []).push(comments[ci]); + attached.add(ci); + } + ci++; + } + return ci; + } + + /** + * Process an array of AST nodes (e.g. params, arguments, body). + * For `body` arrays, comments between elements get both trailing+leading. + * For other arrays, only leading on the next element. + * Empty arrays → inner on parent. + */ + function processArray( + parent: MutableNode, + arr: MutableNode[], + fieldName: string, + ciStart: number, + regionEnd: number + ): number { + let ci = ciStart; + const isBody = fieldName === "body"; + + if (arr.length === 0) { + // Empty array — comments in this region are inner on parent + ci = claim(ci, regionEnd, parent, "innerComments"); + return ci; + } + + // Before first element + ci = claim(ci, arr[0].start, arr[0], "leadingComments"); + + // Process each element + for (let i = 0; i < arr.length; i++) { + visit(arr[i]); + // Advance past comments inside this element + while (ci < comments.length && comments[ci].start < arr[i].end) ci++; + + if (i < arr.length - 1) { + // Gap between arr[i] and arr[i+1] + if (isBody) { + ci = claimBoth(ci, arr[i + 1].start, arr[i], arr[i + 1]); + } else { + ci = claim(ci, arr[i + 1].start, arr[i + 1], "leadingComments"); + } + } + } + + // After last element + const last = arr[arr.length - 1]; + ci = claim(ci, regionEnd, last, "trailingComments"); + + return ci; + } + + /** + * Recursively visit a node and attach comments to it and its descendants. + */ + function visit(node: MutableNode): void { + // Find comment range for this node + let ci = commentLowerBound(comments, node.start); + const hi = commentLowerBound(comments, node.end); + if (ci >= hi) return; // no comments in this node's range + + // Collect child fields in source order: [fieldName, value, startPos] + const fields: [string, MutableNode | MutableNode[], number][] = []; + for (const key of Object.keys(node)) { + if (COMMENT_SKIP_KEYS.has(key)) continue; + const val = node[key]; + if (!val || typeof val !== "object") continue; + if (Array.isArray(val)) { + const astItems = val.filter(isAstNode); + if ( + astItems.length > 0 || + val === node.params || + val === node.arguments || + val === node.elements || + val === node.properties || + val === node.body + ) { + const pos = astItems.length > 0 ? astItems[0].start : node.start; + fields.push([key, astItems, pos]); + } + } else if (isAstNode(val)) { + fields.push([key, val, val.start]); + } + } + fields.sort((a, b) => a[2] - b[2]); + + if (fields.length === 0) { + // Leaf node — all comments are inner + claim(ci, node.end, node, "innerComments"); + return; + } + + // Process gaps between fields and recurse into each field + for (let fi = 0; fi < fields.length; fi++) { + const [fieldName, value] = fields[fi]; + const regionEnd = + fi < fields.length - 1 ? (fields[fi + 1][2] as number) : node.end; + + if (Array.isArray(value)) { + ci = processArray( + node, + value as MutableNode[], + fieldName, + ci, + regionEnd + ); + } else { + const child = value as MutableNode; + // Gap before this child + ci = claim(ci, child.start, child, "leadingComments"); + visit(child); + // Advance past comments inside this child + while (ci < comments.length && comments[ci].start < child.end) ci++; + } + + // Gap between this field and the next (or end of node) + if (fi < fields.length - 1) { + const nextField = fields[fi + 1]; + const nextStart = nextField[2] as number; + + // Get last node of current field and first of next + let lastOfCurrent: MutableNode | undefined; + let firstOfNext: MutableNode | undefined; + + if (Array.isArray(value)) { + const arr = value as MutableNode[]; + if (arr.length > 0) lastOfCurrent = arr[arr.length - 1]; + } else { + lastOfCurrent = value as MutableNode; + } + + const nextVal = nextField[1]; + if (Array.isArray(nextVal)) { + const arr = nextVal as MutableNode[]; + if (arr.length > 0) firstOfNext = arr[0]; + } else { + firstOfNext = nextVal as MutableNode; + } + + if (lastOfCurrent) { + // Between two different fields: trailing on previous only + ci = claim(ci, nextStart, lastOfCurrent, "trailingComments"); + } else if (firstOfNext) { + ci = claim(ci, nextStart, firstOfNext, "leadingComments"); + } else { + // Both sides are empty arrays — inner on parent + ci = claim(ci, nextStart, node, "innerComments"); + } + } + } + + // Remaining unclaimed comments after the last field: + // - If there's a last positioned child, trailing on it + // - Otherwise, inner on the node + if (ci < hi) { + // Find the last positioned child across all fields + let lastChild: MutableNode | undefined; + for (let fi = fields.length - 1; fi >= 0 && !lastChild; fi--) { + const val = fields[fi][1]; + if (Array.isArray(val)) { + const arr = val as MutableNode[]; + if (arr.length > 0) lastChild = arr[arr.length - 1]; + } else { + lastChild = val as MutableNode; + } + } + + while (ci < hi) { + if (!attached.has(ci)) { + if (lastChild) { + (lastChild.trailingComments ??= []).push(comments[ci]); + } else { + (node.innerComments ??= []).push(comments[ci]); + } + attached.add(ci); + } + ci++; + } + } + } + + visit(root); + return comments; +} + +/** + * For nodes with extra.parenthesized, reclaim comments from the previous + * sibling's trailing that fall inside the paren range (between parenStart + * and the node's actual start). + * + * This fixes cases like: a || (COMMENT b || c) where the comment + * between ( and b gets attached as trailing on a instead of leading + * on the parenthesized expression. + */ +function fixupParenthesizedLeading(parent: MutableNode): void { + // Collect all positioned children sorted by start + const children: { key: string; node: MutableNode }[] = []; + for (const key of Object.keys(parent)) { + if (COMMENT_SKIP_KEYS.has(key)) continue; + const val = parent[key]; + if (!val || typeof val !== "object") continue; + if (Array.isArray(val)) { + for (const item of val) { + if (isAstNode(item)) children.push({ key, node: item }); + } + } else if (isAstNode(val)) { + children.push({ key, node: val }); + } + } + children.sort((a, b) => a.node.start - b.node.start); + + // For each parenthesized child, reclaim comments from the preceding child + for (let i = 1; i < children.length; i++) { + const right = children[i].node; + if (!right.extra?.parenthesized) continue; + const parenStart = right.extra.parenStart; + if (typeof parenStart !== "number") continue; + + const left = children[i - 1].node; + // Any comment in the gap between left and the parenthesized right + // belongs to the right operand (Babel semantics) + splitComments( + left, + "trailingComments", + right, + "leadingComments", + (c) => c.start > left.end + ); + } +} + +type CommentKind = "leadingComments" | "trailingComments" | "innerComments"; + +/** + * Split a comment array: comments matching the predicate move to `target[targetKind]`, + * the rest stay on `owner[sourceKind]`. Deletes the source property if all comments move. + */ +function splitComments( + owner: MutableNode, + sourceKind: CommentKind, + target: MutableNode, + targetKind: CommentKind, + predicate: (c: MutableNode) => boolean +): void { + const comments = owner[sourceKind]; + if (!comments) return; + const keep: MutableNode[] = []; + for (const c of comments) { + if (predicate(c)) { + (target[targetKind] ??= []).push(c); + } else { + keep.push(c); + } + } + if (keep.length > 0) owner[sourceKind] = keep; + else delete owner[sourceKind]; +} + +/** + * Forward-scan source for a character, skipping over `//` and `/* * /` comments. + * Returns the index of the first match, or -1 if not found. + */ +function findChar( + source: string, + target: number, + start: number, + end: number +): number { + for (let i = start; i < end; i++) { + const ch = source.charCodeAt(i); + if (ch === target) return i; + if (ch === 47) { + // '/' — possible comment start + const next = source.charCodeAt(i + 1); + if (next === 42) { + const close = source.indexOf("*/", i + 2); + if (close >= 0) i = close + 1; + } else if (next === 47) { + const close = source.indexOf("\n", i + 2); + if (close >= 0) i = close; + } + } + } + return -1; +} + +/** + * For nodes that start with keywords followed by `(` (switch, catch, while, + * for, if), move comments from the first child's leadingComments that are + * between the keyword and `(` into innerComments on the node. + * + * Also handles class/object methods where `async`, `static`, `*` keywords + * precede the key — comments between keywords are inner on the method. + */ +function fixupKeywordToParen(node: MutableNode, source: string): void { + // Determine the first positioned child field + let firstChild: MutableNode | undefined; + const type = node.type; + + if ( + type === "SwitchStatement" || + type === "WhileStatement" || + type === "DoWhileStatement" || + type === "IfStatement" + ) { + firstChild = node.discriminant || node.test || node.consequent || undefined; + } else if (type === "CatchClause") { + firstChild = node.param || node.body || undefined; + } else if (type === "ForStatement") { + firstChild = node.init || node.test || node.update || node.body; + } else if (type === "ForInStatement" || type === "ForOfStatement") { + firstChild = node.left || node.right || node.body; + } else if ( + type === "ClassMethod" || + type === "ClassPrivateMethod" || + type === "ObjectMethod" || + type === "TSDeclareMethod" + ) { + firstChild = node.key; + } + + if (!firstChild?.leadingComments) return; + + // Find `(` between node start and first child start + const openParen = findChar(source, 40, node.start, firstChild.start); + + // For class/object methods: there may be no `(` before the key. + // Instead, look for `*` (generator marker) to split at. + if ( + openParen < 0 && + (type === "ClassMethod" || + type === "ClassPrivateMethod" || + type === "ObjectMethod" || + type === "TSDeclareMethod") + ) { + // Find `*` between node start and key start + const starPos = findChar(source, 42, node.start, firstChild.start); + if (starPos >= 0) { + // Move key.leading comments before `*` to method.inner + splitComments( + firstChild, + "leadingComments", + node, + "innerComments", + (c) => c.end <= starPos + ); + } + return; + } + + if (openParen < 0) return; + + // Move comments before `(` from firstChild.leading to node.inner + splitComments( + firstChild, + "leadingComments", + node, + "innerComments", + (c) => c.end <= openParen + ); +} + +/** + * Fix comment attachment for a single node using source text to find + * punctuation positions (`(`, `)`, `=>`, `*`) that delimit regions. + * Called from postVisitorWalk after children are processed. + * + * Handles function-like nodes (params), call-like nodes (arguments), + * and keyword-to-paren nodes (switch, catch, etc.). + */ +function fixupFunctionCommentsSingle(node: MutableNode, source: string): void { + // Handle switch/catch/while/for: keyword ... (test/discriminant/param) + // Comments between keyword and `(` should be inner on the node. + fixupKeywordToParen(node, source); + + // TryStatement: fix `finally` keyword comments — move handler's trailing + // comments that are before `finally {` to finalizer.leading + if (node.type === "TryStatement" && node.finalizer && node.handler) { + const handler = node.handler; + const finalizer = node.finalizer; + splitComments( + handler, + "trailingComments", + finalizer, + "leadingComments", + (c) => c.end <= finalizer.start && c.start >= handler.end + ); + } + + // CatchClause: handle param (singular) and body with `)` to `{` gap + if (node.type === "CatchClause" && node.param && node.body) { + let closeParen = -1; + for (let i = node.body.start - 1; i >= node.param.end; i--) { + if (source.charCodeAt(i) === 41) { + closeParen = i; + break; + } + } + if (closeParen >= 0) { + // Move param's trailing comments after `)` to body.leading + splitComments( + node.param, + "trailingComments", + node.body, + "leadingComments", + (c) => c.start >= closeParen + ); + } + return; + } + + const hasParams = Array.isArray(node.params); + const hasArgs = Array.isArray(node.arguments); + if (!hasParams && !hasArgs) return; + + const arr: MutableNode[] = hasParams ? node.params : node.arguments; + const body = node.body; + + // Find the opening `(` in the source after the id/callee/key + const searchStart = (node.id || node.key || node.callee)?.end ?? node.start; + const openParen = findChar(source, 40, searchStart, node.end); + if (openParen < 0) return; + + // For empty params: comments between searchStart and body.start should + // be split at openParen: before `(` → trailing on id (if exists) or + // inner on node; after `(` → inner on node. + if (arr.length === 0 && body) { + // Move body-leading comments that are positionally before body into inner + splitComments( + body, + "leadingComments", + node, + "innerComments", + (c) => c.end <= body.start + ); + // Move id trailing comments that are after openParen into inner + const id = node.id || node.key; + if (id) { + splitComments( + id, + "trailingComments", + node, + "innerComments", + (c) => c.start > openParen + ); + } + return; + } + + if (arr.length === 0) { + // Empty params/args — split inner comments at openParen + const owner = node.id || node.key || node.callee; + if (owner) { + splitComments( + node, + "innerComments", + owner, + "trailingComments", + (c) => c.end <= openParen + ); + splitComments( + owner, + "trailingComments", + node, + "innerComments", + (c) => c.start > openParen + ); + } + return; + } + + // Non-empty params: comments between searchStart and first param need + // to be split at openParen. + // - Comments before `(`: stay as trailing on id/callee + // - Comments after `(` but before first param: leading on first param + const firstParam = arr[0]; + const id = node.id || node.key || node.callee; + + // Split comments at the openParen boundary: + // - Comments before `(` stay on id as trailing + // - Comments after `(` should be leading on first param + // First, remove from firstParam.leading any that are before `(` + if (firstParam.leadingComments) { + const keep: MutableNode[] = []; + for (const c of firstParam.leadingComments) { + if (c.start <= openParen) { + if (id) { + // Before `(` — trailing on id/callee/key + if (!id.trailingComments?.includes(c)) { + (id.trailingComments ??= []).push(c); + } + } else { + // No owner (e.g. ArrowFunctionExpression) — inner on node + (node.innerComments ??= []).push(c); + } + } else { + keep.push(c); + } + } + if (keep.length > 0) firstParam.leadingComments = keep; + else delete firstParam.leadingComments; + } + + // Move id trailing comments that are after `(` to first param leading + if (id?.trailingComments) { + const keepTrailing: MutableNode[] = []; + for (const c of id.trailingComments) { + if (c.start > openParen) { + if (!firstParam.leadingComments?.includes(c)) { + (firstParam.leadingComments ??= []).push(c); + } + } else { + keepTrailing.push(c); + } + } + if (keepTrailing.length > 0) id.trailingComments = keepTrailing; + else delete id.trailingComments; + } + + // Fix trailing comma: if there's a `,` between last arg's end and `)`, + // comments after the comma should be inner on the call/new, not trailing + // on the last arg. + if (hasArgs && arr.length > 0 && node.type === "NewExpression") { + const lastArg = arr[arr.length - 1]; + // Find closing `)` by scanning backwards from node end + let closeParenCall = -1; + for (let i = node.end - 1; i >= lastArg.end; i--) { + if (source.charCodeAt(i) === 41) { + closeParenCall = i; + break; + } + } + if (closeParenCall >= 0 && lastArg.trailingComments) { + // Check for trailing comma between lastArg.end and closeParenCall + if (findChar(source, 44, lastArg.end, closeParenCall) >= 0) { + splitComments( + lastArg, + "trailingComments", + node, + "innerComments", + (c) => c.end <= closeParenCall + ); + } + } + } + + // Fix comments around `)` and body: + // - Comments before `)` should be trailing on last param + // - Comments between `)` and body should be inner on function + if (body) { + const lastParam = arr[arr.length - 1]; + let closeParen = -1; + for (let i = body.start - 1; i >= lastParam.end; i--) { + if (source.charCodeAt(i) === 41) { + closeParen = i; + break; + } + } + if (closeParen >= 0) { + // Process body.leadingComments first — move those before `)` to last param + splitComments( + body, + "leadingComments", + lastParam, + "trailingComments", + (c) => c.end <= closeParen + ); + // Then move last param trailing comments after `)` to body.leading + splitComments( + lastParam, + "trailingComments", + body, + "leadingComments", + (c) => c.start >= closeParen + ); + + // For arrow functions: split body.leading at `=>` position. + // Comments between `)` and `=>` → inner on arrow function. + // Comments after `=>` → stay as leading on body. + if (node.type === "ArrowFunctionExpression" && body.leadingComments) { + let arrowPos = -1; + for (let i = closeParen + 1; i < body.start - 1; i++) { + if (source.charCodeAt(i) === 61 && source.charCodeAt(i + 1) === 62) { + arrowPos = i; + break; + } + } + if (arrowPos >= 0) { + splitComments( + body, + "leadingComments", + node, + "innerComments", + (c) => c.end <= arrowPos + ); + } + } + } + } +} + +// ── Main conversion entry point ───────────────────────────────────── + +/** + * Convert an OXC ESTree Program to a Babel-compatible AST in a single pass. + * Uses oxc-parser's built-in Visitor for tree walking (no @babel/traverse overhead) + * and mutates nodes in-place for zero allocation overhead. + */ +export function toBabelAST( + program: Program, + source: string, + isTypeScript?: boolean, + rawComments?: RawComment[] +): ParseResult { + program.start = 0; + program.end = source.length; + + const newlines = getNewlines(source); + resetContext(newlines, isTypeScript ?? false); + + // Single-pass visitor + new Visitor(moduleVisitor).visit(program); + + // Post-process deferred nodes (children are now fully visited) + for (const [key, fn] of postProcessors) { + for (const node of ctx.deferred[key]) fn(node); + } + + // Wrap in Babel's File structure + const prog = program as MutableNode; + if (!prog.directives) prog.directives = []; + delete prog.comments; + + // Attach comments to individual AST nodes and build File.comments + const comments = rawComments + ? attachComments(prog, rawComments, newlines) + : []; + + // Combined post-visitor walk: rename typeArguments + fix function comments + // Pass source only when there are comments (skip fixup work otherwise) + postVisitorWalk(prog, comments.length > 0 ? source : undefined); + + prog.extra = { topLevelAwait: ctx.hasTopLevelAwait }; + + return { + type: "File", + program: prog, + comments, + errors: [], + } as unknown as ParseResult; +} diff --git a/incubator/tools-babel/src/index.ts b/incubator/tools-babel/src/index.ts new file mode 100644 index 000000000..c915c754c --- /dev/null +++ b/incubator/tools-babel/src/index.ts @@ -0,0 +1,27 @@ +export { getBabelConfig, filterConfigPlugins } from "./config"; + +export { toBabelAST } from "./estree"; + +export { initTransformerContext, makeTransformerArgs } from "./options"; +export { hermesParseToAst, oxcParseToAst, parseToAst } from "./parse"; + +export type { ResolvedPlugin, PluginVisitor } from "./plugins"; +export { + isConfigItem, + isPluginObj, + getPluginKey, + getPluginTarget, + updateTransformOptions, +} from "./plugins"; + +export type { + BabelTransformerOptions, + BabelTransformerArgs, + FileContext, + HermesParserOptions, + SrcSyntax, + TraceFunction, + TransformerArgs, + TransformerContext, + TransformerSettings, +} from "./types"; diff --git a/incubator/tools-babel/src/options.ts b/incubator/tools-babel/src/options.ts new file mode 100644 index 000000000..36ca6d0cf --- /dev/null +++ b/incubator/tools-babel/src/options.ts @@ -0,0 +1,120 @@ +import { getTrace } from "@rnx-kit/tools-performance"; +import type { EventFrequency } from "@rnx-kit/tools-performance"; +import path from "node:path"; +import { getBabelConfig } from "./config"; +import type { + TransformerSettings, + TransformerContext, + TransformerArgs, + BabelTransformerArgs, + SrcSyntax, +} from "./types"; + +export const TRACE_DOMAIN = "transform"; + +/** + * Get a trace function for the babel domain with the requested frequency. + * @param frequency + * @returns either a tracing function or a no-op passthrough if tracing is not enabled + */ +export function getPerfTrace(frequency?: EventFrequency) { + return getTrace(TRACE_DOMAIN, frequency); +} + +function getBaseExt(ext: string, fallback?: SrcSyntax): SrcSyntax | undefined { + switch (ext) { + case ".ts": + return "ts"; + case ".tsx": + return "tsx"; + case ".js": + case ".cjs": + case ".mjs": + return "js"; + case ".jsx": + case ".cjsx": + case ".mjsx": + return "jsx"; + default: + return fallback; + } +} + +const NM_REGEX = /[\\/]node_modules[\\/]/; + +/** + * Initialize the context object, filling in the file specific information along with the + * transformer settings. + * @param filename the file being transformed, used to determine the extension and other file specific information + * @param settings common settings for the transformer + * @returns The initialized transformer context or undefined if the file should be skipped. + */ +export function initTransformerContext< + T extends TransformerContext = TransformerContext, +>(filename: string, settings: Partial): T | undefined { + const { + parseFlowDefault = true, + parseFlowWorkspace = false, + parseExtAliases, + parseExtDefault = "js", + } = settings; + const isNodeModule = NM_REGEX.test(filename); + + const ext = path.extname(filename).toLowerCase(); + const srcSyntax: SrcSyntax | undefined = + parseExtAliases?.[ext] ?? getBaseExt(ext, parseExtDefault); + + // invalid extension and no default means this file should be skipped, return undefined to indicate that + if (!srcSyntax) { + return undefined; + } + + const isJsFile = srcSyntax === "js" || srcSyntax === "jsx"; + const flowDefault = isNodeModule ? parseFlowDefault : parseFlowWorkspace; + const mayContainFlow = + isJsFile && + (flowDefault || + filename.endsWith(".flow.js") || + filename.endsWith(".flow.jsx")); + + return { + ...settings, + ext, + srcSyntax, + mayContainFlow, + isNodeModule, + } as T; +} + +/** + * This forms the TransformerArgs type, which does the following: + * - Initialize the context with file specific information and common settings + * - Optionally call the updateContext function allowing the caller to add additional configuration + * - Get the babel config for this file, returning null if the file should be skipped + * @param babelArgs the metro transformer args + * @param settings additional configuration options to mix in + * @param updateContext a function that allows additional configuration to be added to the context + * @returns the transformer arguments or null if babel can't be loaded for the file + */ +export function makeTransformerArgs< + T extends TransformerContext = TransformerContext, +>( + babelArgs: BabelTransformerArgs, + settings?: Partial, + updateContext?: (context: T, args: BabelTransformerArgs) => void +): TransformerArgs | null { + const { filename, src, options } = babelArgs; + const context = initTransformerContext(filename, { + ...settings, + }); + if (context) { + if (updateContext) { + updateContext(context, babelArgs); + } + const config = getBabelConfig(babelArgs, context); + if (config) { + return { filename, src, options, context, config }; + } + } + return null; +} diff --git a/incubator/tools-babel/src/parse.ts b/incubator/tools-babel/src/parse.ts new file mode 100644 index 000000000..a8282af7a --- /dev/null +++ b/incubator/tools-babel/src/parse.ts @@ -0,0 +1,93 @@ +import type { Node } from "@babel/core"; +import { parseSync as parseSyncBabel } from "@babel/core"; +import type { TraceFunction } from "@rnx-kit/tools-performance"; +import type { OxcError } from "oxc-parser"; +import { toBabelAST } from "./estree"; +import { getPerfTrace } from "./options"; +import type { HermesParserOptions, TransformerArgs } from "./types"; + +export function isFlowError(errors: OxcError[]): boolean { + return errors.some((e) => e.message === "Flow is not supported"); +} + +/** + * Parse a file to an AST using the hermes parser. Matches the signature from the hermes-parser package + * @param src incoming source file to parse + * @param options hermes parser options + */ +export function hermesParse(src: string, options?: HermesParserOptions): Node { + return require("hermes-parser").parse(src, options); +} + +export function oxcParseToAst( + { src, filename, context, config }: TransformerArgs, + trace?: TraceFunction +): Node | null { + const { parseSync } = require("oxc-parser"); + const { disableOxcParser: parseDisableOxc } = context; + trace ??= getPerfTrace(); + // setting disabled specifically turns off auto-detection, otherwise avoid flow files + const disabled = parseDisableOxc ?? context.mayContainFlow; + if (disabled) { + return null; + } + const isTypeScript = + context.srcSyntax === "ts" || context.srcSyntax === "tsx"; + + const oxcResult = trace("parse:oxc:native", parseSync, filename, src, { + sourceType: config.sourceType ?? "unambiguous", + lang: context.srcSyntax, + astType: isTypeScript ? "ts" : "js", + }); + + const errors = oxcResult.errors; + if (errors.length > 0) { + if (!isFlowError(errors)) { + for (const e of errors) { + console.error(e.codeframe); + } + throw new Error(`Failed to parse '${filename}' because of above errors`); + } + } else { + return trace( + "parse:oxc:convert-ast", + toBabelAST, + oxcResult.program, + src, + isTypeScript, + oxcResult.comments + ); + } + return null; +} + +export function hermesParseToAst({ + src, + context, + config, +}: TransformerArgs): Node | null { + const { disableHermesParser: parseDisableHermes } = context; + if (parseDisableHermes) { + return null; + } + return hermesParse(src, { + babel: true, + reactRuntimeTarget: "19", + sourceType: config.sourceType ?? undefined, + }); +} + +export function parseToAst(args: TransformerArgs): Node | null { + const trace = getPerfTrace(); + const ast = + trace("parse:oxc", oxcParseToAst, args, trace) ?? + trace("parse:hermes", hermesParseToAst, args); + if (ast) { + return ast; + } + + const { src, config } = args; + + // fall through to babel if all else fails (or if other modes are disabled) + return trace("parse:babel", parseSyncBabel, src, config); +} diff --git a/incubator/tools-babel/src/plugins.ts b/incubator/tools-babel/src/plugins.ts new file mode 100644 index 000000000..35a19df51 --- /dev/null +++ b/incubator/tools-babel/src/plugins.ts @@ -0,0 +1,204 @@ +import type { + PluginItem, + PluginObj, + ConfigItem, + PluginTarget, + TransformOptions, +} from "@babel/core"; +import { getTrace, isTrackingEnabled } from "@rnx-kit/tools-performance"; + +export const PLUGIN_DOMAIN = "babel-plugin"; + +export function shouldInstrumentPlugins() { + return isTrackingEnabled(PLUGIN_DOMAIN, "high"); +} + +export type ResolvedPlugin = { + key: string | undefined | null; + manipulateOptions?: PluginObj["manipulateOptions"]; + post?: PluginObj["post"]; + pre?: PluginObj["pre"]; + visitor: PluginObj["visitor"]; + options: object; +}; + +// oxlint-disable-next-line typescript/no-explicit-any +type AnyFuncType = (...args: any[]) => void; + +function isPlainObject(value: unknown): value is Record { + return value != null && typeof value === "object" && !Array.isArray(value); +} + +export function isConfigItem(plugin: PluginItem): plugin is ConfigItem { + return isPlainObject(plugin) && "value" in plugin; +} + +export function isPluginObj(plugin: PluginItem): plugin is PluginObj { + return isPlainObject(plugin) && "visitor" in plugin; +} + +export function getPluginTarget(plugin: PluginItem): PluginTarget | undefined { + if (plugin != null) { + if (Array.isArray(plugin)) { + return plugin[0]; + } else if (isConfigItem(plugin)) { + return plugin.value; + } else if (!isPluginObj(plugin)) { + return plugin; + } + } + return undefined; +} + +export function getPluginKey(plugin: PluginItem): string | null | undefined { + if (isPluginObj(plugin)) { + return (plugin as ResolvedPlugin).key; + } + return undefined; +} + +/** + * Create the trace factory that wraps visitor methods for instrumentation + */ +export const pluginTraceFactory = (() => { + let factory: TransformOptions["wrapPluginVisitorMethod"] | null = null; + return () => { + if (factory) { + return factory; + } + // this will only be enabled if we are requesting high frequency tracing + const trace = getTrace(PLUGIN_DOMAIN, "high"); + return (factory = ( + key: string, + _visitorType: "enter" | "exit", + fn: AnyFuncType + ) => { + function thisWrapper(this: ThisParameterType, ...args: Parameters) { + return trace(key, () => fn.apply(this, args)); + } + return thisWrapper; + }); + }; +})(); + +/** + * Helper to avoid creating new arrays unless necessary. This is the value setter with an update cache + */ +type ArrayUpdates = Record; + +function getUpdatedArray(base: T[], updates: ArrayUpdates): T[] { + if (Object.keys(updates).length === 0) { + return base; + } + const result: T[] = []; + for (let i = 0; i < base.length; i++) { + const entry = Object.hasOwn(updates, i) ? updates[i] : base[i]; + if (entry !== null) { + result.push(entry); + } + } + return result; +} + +/** + * A visitor function which will be called for each known plugin in the config, allowing edits to the plugin + * configuration. + * - return the same plugin item to keep it unchanged, will be compared via === + * - return a new plugin item to replace it in the config + * - return null to remove it from the config + */ +export type PluginVisitor = ( + pluginItem: PluginItem, + preset?: boolean +) => PluginItem | null; + +/** + * Modify the plugins array based on the provided visitor function, this will only create a new array if changes are made. + * Updates can be detected by reference equality checks on the plugin array + * @param plugins list of plugins to (potentially) modify + * @param visitor visitor function called for each known plugin in the config + * @param cache plugin cache used to identify plugins and return their module name + * @returns either the original array (if unchanged) or a new array. + */ +export function updatePlugins( + plugins: PluginItem[], + visitor: PluginVisitor, + preset = false +): PluginItem[] { + const updates: ArrayUpdates = {}; + + // go through and record any updates based on the visitor function + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + const newValue = visitor(plugin, preset); + if (newValue !== plugin) { + updates[i] = newValue; + } + } + + // return the new array if there were changes, otherwise return the original to preserve reference equality + return getUpdatedArray(plugins, updates); +} + +/** + * Update the plugins in a babel transform options object based on the provided visitor function, this will only + * create a new config object if changes are made. + * @param options transform options to update + * @param visitor visitor function called for each known plugin in the config + * @param cache plugin cache used to identify plugins and return their module name + * @returns either the original config (if unchanged) or a new config object. + */ +export function updateTransformOptions( + options: TransformOptions, + visitor: PluginVisitor +): TransformOptions { + const { plugins, overrides, presets } = options; + + // check the plugin array for updates if it exists + const newPlugins = plugins ? updatePlugins(plugins, visitor) : plugins; + const newPresets = presets ? updatePlugins(presets, visitor, true) : presets; + let newOverrides = overrides; + + if (overrides && overrides.length > 0) { + const updates: ArrayUpdates = {}; + for (let i = 0; i < overrides.length; i++) { + const override = overrides[i]; + // process the override with the visitor, this will return a new config if changes were made. + let newOverride: TransformOptions | null = updateTransformOptions( + override, + visitor + ); + + // see if all plugins were removed and it is effectively empty. + if (newOverride?.plugins?.length === 0) { + const keyLength = Object.keys(newOverride).length; + // if there are no plugins and the only other key is test, then this override will never apply so we can remove it + if (keyLength === 1 || (keyLength === 2 && newOverride.test)) { + newOverride = null; + } + } + + // record the change if the override was modified or removed + if (newOverride !== override) { + updates[i] = newOverride; + } + } + // finalize the overrides array, modifying it if there were changes. + newOverrides = getUpdatedArray(overrides, updates); + } + + if ( + newPlugins !== plugins || + newOverrides !== overrides || + newPresets !== presets + ) { + return { + ...options, + presets: newPresets, + plugins: newPlugins, + overrides: newOverrides, + }; + } + + return options; +} diff --git a/incubator/tools-babel/src/types.ts b/incubator/tools-babel/src/types.ts new file mode 100644 index 000000000..e291f9e43 --- /dev/null +++ b/incubator/tools-babel/src/types.ts @@ -0,0 +1,145 @@ +import type { PluginItem, TransformOptions } from "@babel/core"; +import type { BabelTransformerArgs as MetroTransformerArgs } from "metro-babel-transformer"; + +/** + * Define the options type for metro babel transformers. Internally the default transformer checks + * options.hot to determine whether to include the hot module replacement plugin but it is missing + * from the types. + */ +export type BabelTransformerOptions = MetroTransformerArgs["options"] & { + hot?: boolean; +}; + +/** + * Redeclare the transformer args type to include the typed plugins field and the adjusted options type. + */ +export type BabelTransformerArgs = { + readonly filename: string; + readonly options: BabelTransformerOptions; + readonly plugins?: PluginItem[]; + readonly src: string; +}; + +/** + * Modified transformer args, this can be passed to something that expects the original args, but while within + * internal routines this allows the same args object to be used and mutated in place to avoid unnecessary copying + * and object creation. + */ +export type TransformerArgs = + { + /** Filename of the source file being transformed */ + readonly filename: string; + + /** Metro's babel transformer options */ + readonly options: BabelTransformerOptions; + + /** Babel config, will already have the plugins applied */ + config: TransformOptions; + + /** current value for src, may have been modified during the transformation process */ + src: string; + + /** info and state about the file being transformed */ + context: T; + }; + +/** + * The context for a given transformation pass, combination of file specific information and broader settings + */ +export type TransformerContext = FileContext & TransformerSettings; + +export type SrcSyntax = "js" | "jsx" | "ts" | "tsx"; + +/** + * Information about a file that is being transformed, these options may be mutated as transformation + * progresses to reflect changes to the source or to track state about the file. + */ +export type FileContext = { + /** file extension, lower case */ + ext: string; + + /** syntax type of the source file */ + srcSyntax: SrcSyntax; + + /** if a native tool has modified source, a map in serialized JSON format can be added */ + map?: string; + + /** May contain flow syntax */ + mayContainFlow: boolean; + + /** Is this file under node_modules */ + isNodeModule: boolean; +}; + +/** + * Settings that persist being transformation passes and are not specific to a single file. + */ +export type TransformerSettings = { + /** values to add to the babel config's caller structure */ + configCallerMixins?: Record; + + /** key values for plugins that should be disabled in the config */ + configDisabledPlugins?: Set; + + /** disable the oxc parser */ + disableOxcParser?: boolean; + + /** disable the hermes parser */ + disableHermesParser?: boolean; + + /** Starting flow code state for .js(x) files */ + parseFlowDefault?: boolean; + + /** whether this workspace uses flow */ + parseFlowWorkspace?: boolean; + + /** Extension for unknown file types, if unset will return a null ast */ + parseExtDefault?: SrcSyntax; + + /** Syntax aliases, e.g. { ".svg": "jsx" } to treat svg as jsx files */ + parseExtAliases?: Record; +}; + +/** + * Signature for a trace function that will record information about a call and its duration. This is overloaded to + * support both sync and async functions and will forward the trailing args to the function being measured. + * This allows use with and without closures, e.g. both of these work: + * trace("myFunction", () => myFunction(arg1, arg2)); + * trace("myFunction", myFunction, arg1, arg2); + */ +export type TraceFunction = { + // oxlint-disable-next-line typescript/no-explicit-any + Promise>( + name: string, + fn: TFunc, + ...args: Parameters + ): ReturnType; + // oxlint-disable-next-line typescript/no-explicit-any + any>( + name: string, + fn: TFunc, + ...args: Parameters + ): ReturnType; +}; + +/** + * Types for hermes parser, defined here as they are in flow types in the hermes-parser package + */ +export type HermesParserOptions = { + allowReturnOutsideFunction?: boolean; + babel?: boolean; + flow?: "all" | "detect"; + enableExperimentalComponentSyntax?: boolean; + enableExperimentalFlowMatchSyntax?: boolean; + enableExperimentalFlowRecordSyntax?: boolean; + reactRuntimeTarget?: "18" | "19"; + sourceFilename?: string; + sourceType?: "module" | "script" | "unambiguous"; + tokens?: boolean; + transformOptions?: { + TransformEnumSyntax?: { + enable: boolean; + getRuntime?: () => unknown; + }; + }; +}; diff --git a/incubator/tools-babel/test/analysis.ts b/incubator/tools-babel/test/analysis.ts new file mode 100644 index 000000000..3ad7aa97a --- /dev/null +++ b/incubator/tools-babel/test/analysis.ts @@ -0,0 +1,164 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyNode = Record; + +/** + * Compare two AST nodes recursively and collect differences. + * Ignores loc, start, end, and comments fields. + */ +export function diffAst( + oxc: AnyNode | null | undefined, + babel: AnyNode | null | undefined, + nodePath: string, + diffs: string[], + maxDiffs = 30 +): void { + if (diffs.length >= maxDiffs) return; + + if (oxc === babel) return; + if (oxc == null && babel == null) return; + + if (oxc == null || babel == null) { + diffs.push( + `${nodePath}: oxc=${JSON.stringify(oxc)}, babel=${JSON.stringify(babel)}` + ); + return; + } + + if (typeof oxc !== typeof babel) { + diffs.push( + `${nodePath}: type mismatch oxc=${typeof oxc} babel=${typeof babel}` + ); + return; + } + + if (typeof oxc !== "object") { + if (oxc !== babel) { + diffs.push( + `${nodePath}: oxc=${JSON.stringify(oxc)}, babel=${JSON.stringify(babel)}` + ); + } + return; + } + + if (Array.isArray(oxc) && Array.isArray(babel)) { + if (oxc.length !== babel.length) { + diffs.push( + `${nodePath}: array length oxc=${oxc.length}, babel=${babel.length}` + ); + } + const len = Math.min(oxc.length, babel.length); + for (let i = 0; i < len; i++) { + diffAst(oxc[i], babel[i], `${nodePath}[${i}]`, diffs, maxDiffs); + } + return; + } + + // skip fields that are expected to differ + const skip = new Set([ + "loc", + "start", + "end", + "comments", + "leadingComments", + "trailingComments", + "innerComments", + "tokens", + ]); + + const allKeys = new Set([...Object.keys(oxc), ...Object.keys(babel)]); + for (const key of allKeys) { + if (skip.has(key)) continue; + if (!(key in oxc) && key in babel) { + // babel has a field oxc doesn't + const val = babel[key]; + // ignore undefined/null fields in babel + if (val != null) { + diffs.push( + `${nodePath}.${key}: missing in oxc, babel=${JSON.stringify(val).slice(0, 80)}` + ); + } + } else if (key in oxc && !(key in babel)) { + const val = (oxc as AnyNode)[key]; + if (val != null) { + diffs.push( + `${nodePath}.${key}: extra in oxc=${JSON.stringify(val).slice(0, 80)}` + ); + } + } else { + diffAst( + (oxc as AnyNode)[key], + (babel as AnyNode)[key], + `${nodePath}.${key}`, + diffs, + maxDiffs + ); + } + } +} + +/** + * Fields that are expected to differ between OXC and Babel ASTs. + * Comments are handled separately and loc/position fields vary by design. + */ +const IGNORED_FIELDS = new Set([ + "loc", + "start", + "end", + "comments", + "leadingComments", + "trailingComments", + "innerComments", + "tokens", +]); + +/** + * Recursively compare two AST nodes and return the number of differences. + */ +export function countDiffs( + oxc: AnyNode | null | undefined, + babel: AnyNode | null | undefined, + maxDiffs = 100 +): number { + let count = 0; + + function walk( + a: AnyNode | null | undefined, + b: AnyNode | null | undefined + ): void { + if (count >= maxDiffs) return; + if (a === b) return; + if (a == null && b == null) return; + if (a == null || b == null) { + count++; + return; + } + if (typeof a !== typeof b) { + count++; + return; + } + if (typeof a !== "object") { + if (a !== b) count++; + return; + } + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) count++; + const len = Math.min(a.length, b.length); + for (let i = 0; i < len; i++) walk(a[i], b[i]); + return; + } + const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]); + for (const key of allKeys) { + if (IGNORED_FIELDS.has(key)) continue; + if (!(key in a) && key in b) { + if (b[key] != null) count++; + } else if (key in a && !(key in b)) { + if ((a as AnyNode)[key] != null) count++; + } else { + walk((a as AnyNode)[key], (b as AnyNode)[key]); + } + } + } + + walk(oxc as AnyNode, babel as AnyNode); + return count; +} diff --git a/incubator/tools-babel/test/ast.test.ts b/incubator/tools-babel/test/ast.test.ts new file mode 100644 index 000000000..2acf49b8c --- /dev/null +++ b/incubator/tools-babel/test/ast.test.ts @@ -0,0 +1,176 @@ +/** + * Diagnostic test that finds structural AST differences between OXC and Babel + * on a single simple non-comment JS fixture, reporting all differences. + */ +import { ok } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { formatAsTable } from "../../tools-performance/src/table"; +import type { AnyNode } from "./analysis"; +import { diffAst } from "./analysis"; +import type { FileData } from "./fixtures"; +import { getFixtures, getRealWorldFixtures } from "./fixtures"; + +const fileSets = ["js-no-comments", "js-comments", "ts", "realworld"] as const; + +const fixtures = getFixtures(); +const realWorldFixtures = getRealWorldFixtures(); +const fileCache: Record = {}; +const closeThreshold = 5; // max number of differences to be considered "close" + +function getFile(set: (typeof fileSets)[number], file: string): FileData { + if (!fileCache[file]) { + fileCache[file] = + set === "realworld" + ? realWorldFixtures.getFileData(file) + : fixtures.getFileData(file); + } + return fileCache[file]; +} + +function getFiles(set: (typeof fileSets)[number]): string[] { + return set === "realworld" + ? realWorldFixtures.getFiles() + : fixtures.getFiles(set); +} + +function getSrc(set: (typeof fileSets)[number], file: string): string { + return set === "realworld" + ? realWorldFixtures.getSrc(file) + : fixtures.getSrc(file); +} + +describe("AST diff diagnostic", () => { + it("can parse ASTs with oxc successfully", () => { + type RowStats = [string, number, number, number, string, string, string]; + const columns = [ + "Type", + "Total", + "Failed OXC", + "Failed Babel", + "OXC Time", + "Babel Time", + "OXC Speed X", + ]; + const stats: RowStats[] = []; + + for (const set of fileSets) { + const files = getFiles(set); + let totalFiles = 0; + let failedOxc = 0; + let failedBabel = 0; + let oxcTime = 0; + let babelTime = 0; + + if (!files) { + throw new Error(`File set not found: ${set}`); + } + for (const filename of files) { + const file = getFile(set, filename); + // warm up source read + getSrc(set, filename); + totalFiles++; // total + const startBabel = performance.now(); + const babelAst = file.babelAst; + if (babelAst == null) failedBabel++; + babelTime += performance.now() - startBabel; + + const startOxc = performance.now(); + const oxcAst = file.oxcAst; + if (oxcAst == null) failedOxc++; + oxcTime += performance.now() - startOxc; + } + stats.push([ + set, + totalFiles, + failedOxc, + failedBabel, + oxcTime.toFixed(1), + babelTime.toFixed(1), + `${(babelTime / oxcTime).toFixed(1)}x`, + ]); + } + console.log("AST Parsing Results:"); + console.log(formatAsTable(stats, { columns })); + for (const [set, total, failedOxc, failedBabel] of stats) { + ok( + failedOxc === 0, + `OXC failed to parse ${failedOxc}/${total} files in set ${set}` + ); + ok( + failedBabel === 0, + `Babel failed to parse ${failedBabel}/${total} files in set ${set}` + ); + } + }); + + it("find all AST differences for non-comment JS fixtures", () => { + const astColumns = ["Type", "Total", "Exact", "Close", "Different"]; + const astStats: [string, number, number, number, number][] = []; + const diffCounts: Record = {}; + const patterns: Record = {}; + const examples: Record = {}; + + for (const set of fileSets) { + const files = getFiles(set); + if (!files) { + throw new Error(`File set not found: ${set}`); + } + let total = 0; + let exact = 0; + let close = 0; + let different = 0; + + for (const filename of files) { + const file = getFile(set, filename); + const babelAst = file.babelAst; + const oxcAst = file.oxcAst; + if (!babelAst || !oxcAst) continue; + + total++; + const diffs: string[] = []; + diffAst(oxcAst as AnyNode, babelAst as AnyNode, "File", diffs); + if (diffs.length === 0) { + exact++; + } else { + if (diffs.length <= closeThreshold) { + close++; + } else { + different++; + } + for (const d of diffs) { + // categorize by the last property name in the path + const match = d.match(/\.(\w+):/); + const category = match ? match[1] : "other"; + diffCounts[category] = (diffCounts[category] || 0) + 1; + + // normalize the path to just field/type pattern + const normalized = d + .replace(/\[\d+\]/g, "[]") + .replace(/File\.program\.body\[\].*\./, "..."); + const key = normalized.slice(0, 120); + patterns[key] = (patterns[key] || 0) + 1; + if (!examples[key]) examples[key] = `${file}: ${d}`; + } + } + } + astStats.push([set, total, exact, close, different]); + } + console.log("AST Comparison Results:"); + console.log(formatAsTable(astStats, { columns: astColumns })); + + console.log("\nDifference categories (most common first):"); + const sorted = Object.entries(diffCounts).sort((a, b) => b[1] - a[1]); + for (const [category, count] of sorted.slice(0, 20)) { + console.log(` ${category}: ${count}`); + } + + console.log("\nTop diff patterns:"); + const sortedPatterns = Object.entries(patterns).sort((a, b) => b[1] - a[1]); + for (const [pattern, count] of sortedPatterns.slice(0, 25)) { + console.log(` [${count}x] ${pattern}`); + console.log(` e.g. ${examples[pattern]?.slice(0, 150)}`); + } + + ok(true); + }); +}); diff --git a/incubator/tools-babel/test/fixtures.ts b/incubator/tools-babel/test/fixtures.ts new file mode 100644 index 000000000..ed72fefa6 --- /dev/null +++ b/incubator/tools-babel/test/fixtures.ts @@ -0,0 +1,219 @@ +import { parseSync, transformFromAstSync } from "@babel/core"; +import type { Node } from "@babel/core"; +import { generate } from "@babel/generator"; +import { lazyInit } from "@rnx-kit/reporter"; +import { + getFixtures as getTestFixtures, + type FixtureSetName, +} from "@rnx-kit/test-fixtures"; +import fs from "node:fs"; +import path from "node:path"; +import { makeTransformerArgs } from "../src/options.ts"; +import { oxcParseToAst } from "../src/parse.ts"; +import type { + BabelTransformerArgs, + BabelTransformerOptions, + TransformerArgs, + TransformerSettings, +} from "../src/types.ts"; + +export const stockSettings: TransformerSettings = {}; + +export function createBabelTransformerArgs( + filename: string, + src: string | undefined, + overrides: Partial = {} +): BabelTransformerArgs { + src ??= fs.readFileSync(filename, "utf8"); + return { + filename, + src, + options: { + dev: false, + hot: false, + minify: false, + platform: "ios", + enableBabelRCLookup: true, + enableBabelRuntime: true, + publicPath: "/", + globalPrefix: "", + unstable_transformProfile: "default", + experimentalImportSupport: false, + projectRoot: process.cwd(), + ...overrides, + }, + plugins: [], + } as BabelTransformerArgs; +} + +function isTsFile(filePath: string): boolean { + return filePath.endsWith(".ts") || filePath.endsWith(".tsx"); +} + +function isJsFile(filePath: string): boolean { + return filePath.endsWith(".js") || filePath.endsWith(".jsx"); +} + +export const getFixtures = lazyInit(() => createFixtureWrapper("language")); + +export const getRealWorldFixtures = lazyInit(() => + createFixtureWrapper("realworld") +); + +export function createFixtureWrapper(name: FixtureSetName) { + const base = getTestFixtures(name); + const { dir, files } = base; + const getSrc = base.getSrc.bind(base); + + const filesets: Record = {}; + + function getBabelArgs( + file: string, + overrides: Partial = {} + ): BabelTransformerArgs { + const filename = path.join(dir, file); + const src = getSrc(file); + return createBabelTransformerArgs(filename, src, overrides); + } + + function getFiles( + fileset?: "js" | "js-comments" | "js-no-comments" | "ts" + ): string[] { + if (fileset) { + switch (fileset) { + case "js": + return (filesets["js"] ??= files.filter(isJsFile)); + case "js-comments": + return (filesets["js-comments"] ??= files.filter( + (f) => isJsFile(f) && f.startsWith("comments-") + )); + case "js-no-comments": + return (filesets["js-no-comments"] ??= files.filter( + (f) => isJsFile(f) && !f.startsWith("comments-") + )); + case "ts": + return (filesets["ts"] ??= files.filter(isTsFile)); + } + } + return files; + } + + function getFileData( + file: string, + overrides: Partial = {} + ): FileData { + const babelArgs = getBabelArgs(file, overrides); + return new FileData(babelArgs); + } + + return { + dir, + files, + getFileData, + getFiles, + getSrc, + getBabelArgs, + }; +} + +export class FileData { + babelArgs: BabelTransformerArgs; + error?: Error; + private _args?: TransformerArgs; + private _babelAst?: Node | null; + private _oxcAst?: Node | null; + private _babelTransformedAst?: Node | null; + private _oxcTransformedAst?: Node | null; + private _srcBabel?: string | null; + private _srcOxc?: string | null; + private _srcTransformedBabel?: string | null; + private _srcTransformedOxc?: string | null; + + constructor(babelArgs: BabelTransformerArgs) { + this.babelArgs = babelArgs; + } + + get args(): TransformerArgs { + return (this._args ??= makeTransformerArgs(this.babelArgs, stockSettings)!); + } + + get babelAst(): Node | null { + if (this._babelAst === undefined) { + const args = this.args; + this._babelAst = args ? parseSync(args.src, args.config) : null; + } + return this._babelAst; + } + + get oxcAst(): Node | null { + if (this._oxcAst === undefined) { + this._oxcAst = this.args ? oxcParseToAst(this.args) : null; + } + return this._oxcAst; + } + + get srcBabel(): string | null { + if (this._srcBabel === undefined) { + this._srcBabel = this.babelAst ? generate(this.babelAst).code : null; + } + return this._srcBabel; + } + + get srcOxc(): string | null { + if (this._srcOxc === undefined) { + this._srcOxc = this.oxcAst ? generate(this.oxcAst).code : null; + } + return this._srcOxc; + } + + get babelTransformedAst(): Node | null { + if (this._babelTransformedAst === undefined) { + try { + this._babelTransformedAst = this.babelAst + ? (transformFromAstSync( + this.babelAst, + this.args.src, + this.args.config + )?.ast ?? null) + : null; + } catch (err) { + this._babelTransformedAst = null; + this.error = err as Error; + } + } + return this._babelTransformedAst; + } + + get oxcTransformedAst(): Node | null { + if (this._oxcTransformedAst === undefined) { + try { + this._oxcTransformedAst = this.oxcAst + ? (transformFromAstSync(this.oxcAst, this.args.src, this.args.config) + ?.ast ?? null) + : null; + } catch (err) { + this._oxcTransformedAst = null; + this.error ??= err as Error; + } + } + return this._oxcTransformedAst; + } + + get srcTransformedBabel(): string | null { + if (this._srcTransformedBabel === undefined) { + this._srcTransformedBabel = this.babelTransformedAst + ? generate(this.babelTransformedAst).code + : null; + } + return this._srcTransformedBabel; + } + + get srcTransformedOxc(): string | null { + if (this._srcTransformedOxc === undefined) { + this._srcTransformedOxc = this.oxcTransformedAst + ? generate(this.oxcTransformedAst).code + : null; + } + return this._srcTransformedOxc; + } +} diff --git a/incubator/tools-babel/test/plugins.test.ts b/incubator/tools-babel/test/plugins.test.ts new file mode 100644 index 000000000..ea9aa0ed2 --- /dev/null +++ b/incubator/tools-babel/test/plugins.test.ts @@ -0,0 +1,200 @@ +import type { ConfigItem, PluginItem, PluginObj } from "@babel/core"; +import { deepEqual, equal, ok } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + getPluginKey, + getPluginTarget, + isConfigItem, + isPluginObj, + pluginTraceFactory, +} from "../src/plugins"; + +// ── Test helpers ───────────────────────────────────────────────────── + +const pluginFn = () => ({ visitor: {} }); + +const samplePluginObj: PluginObj = { + visitor: { + Identifier() { + // intentionally empty for testing + }, + }, +}; + +const sampleConfigItem = { + value: pluginFn, + options: {}, + dirname: "/test", + file: undefined, +} as unknown as ConfigItem; + +// ── isConfigItem ───────────────────────────────────────────────────── + +describe("isConfigItem", () => { + it("returns true for an object with a value property", () => { + ok(isConfigItem(sampleConfigItem)); + }); + + it("returns false for a plain plugin object (visitor)", () => { + equal(isConfigItem(samplePluginObj as PluginItem), false); + }); + + it("returns false for a function", () => { + equal(isConfigItem(pluginFn), false); + }); + + it("returns false for a string", () => { + equal(isConfigItem("@babel/plugin-transform-arrow-functions"), false); + }); + + it("returns false for a tuple", () => { + equal(isConfigItem([pluginFn, {}]), false); + }); + + it("returns false for null/undefined wrapped in array", () => { + equal(isConfigItem([null as unknown as string]), false); + }); +}); + +// ── isPluginObj ────────────────────────────────────────────────────── + +describe("isPluginObj", () => { + it("returns true for an object with a visitor property", () => { + ok(isPluginObj(samplePluginObj as PluginItem)); + }); + + it("returns false for a ConfigItem", () => { + equal(isPluginObj(sampleConfigItem), false); + }); + + it("returns false for a function", () => { + equal(isPluginObj(pluginFn), false); + }); + + it("returns false for a string", () => { + equal(isPluginObj("@babel/plugin-transform-arrow-functions"), false); + }); + + it("returns false for a tuple", () => { + equal(isPluginObj([pluginFn, {}]), false); + }); +}); + +// ── getPluginTarget ────────────────────────────────────────────────── + +describe("getPluginTarget", () => { + it("returns the function for a bare function plugin", () => { + equal(getPluginTarget(pluginFn), pluginFn); + }); + + it("returns the string for a string plugin", () => { + equal( + getPluginTarget("@babel/plugin-transform-arrow-functions"), + "@babel/plugin-transform-arrow-functions" + ); + }); + + it("returns the first element for a tuple plugin", () => { + equal(getPluginTarget([pluginFn, { loose: true }]), pluginFn); + }); + + it("returns the value from a ConfigItem", () => { + equal(getPluginTarget(sampleConfigItem), pluginFn); + }); + + it("returns undefined for a PluginObj (resolved plugin)", () => { + equal(getPluginTarget(samplePluginObj as PluginItem), undefined); + }); + + it("returns undefined for null", () => { + equal(getPluginTarget(null as unknown as PluginItem), undefined); + }); + + it("returns undefined for undefined", () => { + equal(getPluginTarget(undefined as unknown as PluginItem), undefined); + }); +}); + +// ── getPluginKey ───────────────────────────────────────────────────── + +describe("getPluginKey", () => { + it("returns the key from a resolved plugin with a key", () => { + const resolved = { + key: "transform-arrow-functions", + visitor: {}, + options: {}, + }; + equal( + getPluginKey(resolved as unknown as PluginItem), + "transform-arrow-functions" + ); + }); + + it("returns null for a resolved plugin with null key", () => { + const resolved = { key: null, visitor: {}, options: {} }; + equal(getPluginKey(resolved as unknown as PluginItem), null); + }); + + it("returns undefined for a resolved plugin with undefined key", () => { + const resolved = { key: undefined, visitor: {}, options: {} }; + equal(getPluginKey(resolved as unknown as PluginItem), undefined); + }); + + it("returns undefined for a non-PluginObj", () => { + equal(getPluginKey(pluginFn), undefined); + }); + + it("returns undefined for a string plugin", () => { + equal(getPluginKey("@babel/plugin-transform-arrow-functions"), undefined); + }); + + it("returns undefined for a tuple plugin", () => { + equal(getPluginKey([pluginFn, {}]), undefined); + }); +}); + +// ── pluginTraceFactory ─────────────────────────────────────────────── + +describe("pluginTraceFactory", () => { + it("returns a wrapper function", () => { + const wrapper = pluginTraceFactory(); + equal(typeof wrapper, "function"); + }); + + it("returns the same factory on subsequent calls", () => { + const wrapper1 = pluginTraceFactory(); + const wrapper2 = pluginTraceFactory(); + equal(wrapper1, wrapper2); + }); + + it("wraps a visitor method and preserves its behavior", () => { + const factory = pluginTraceFactory(); + const calls: string[] = []; + const original = (arg: string) => { + calls.push(arg); + }; + const wrapped = factory("my-plugin", "enter", original); + wrapped("hello"); + wrapped("world"); + deepEqual(calls, ["hello", "world"]); + }); + + it("wraps a visitor method and preserves return value", () => { + const factory = pluginTraceFactory(); + const original = (x: number) => x * 2; + const wrapped = factory("my-plugin", "enter", original); + equal(wrapped(21), 42); + }); + + it("wraps a visitor method and preserves this binding", () => { + const factory = pluginTraceFactory(); + const capture: { value: unknown } = { value: undefined }; + const original = function (this: unknown) { + capture.value = this; + }; + const wrapped = factory("my-plugin", "enter", original); + const context = { name: "test" }; + wrapped.call(context); + equal(capture.value, context); + }); +}); diff --git a/incubator/tools-babel/test/realworld.test.ts b/incubator/tools-babel/test/realworld.test.ts new file mode 100644 index 000000000..19f39263a --- /dev/null +++ b/incubator/tools-babel/test/realworld.test.ts @@ -0,0 +1,130 @@ +/** + * Real-world fixture tests: compare OXC-parsed ASTs and transformed output + * against Babel reference for actual React Native component files. + * + * These fixtures contain patterns found in production RN code: TypeScript + * interfaces, JSX components, hooks, native modules, theming, etc. + */ +import { equal, ok } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { countDiffs, diffAst, type AnyNode } from "./analysis"; +import { getRealWorldFixtures, type FileData } from "./fixtures"; + +const fixtures = getRealWorldFixtures(); +const fileCache: Record = {}; + +function getFile(file: string): FileData { + return (fileCache[file] ??= fixtures.getFileData(file)); +} + +describe("Real-world fixtures", () => { + describe("AST comparison", () => { + it("all files parse with OXC", () => { + let parsed = 0; + let failed = 0; + for (const file of fixtures.files) { + const fd = getFile(file); + if (fd.oxcAst) { + parsed++; + } else { + failed++; + console.log(` FAIL parse: ${file}`); + } + } + console.log( + `Real-world parse: ${parsed}/${fixtures.files.length} succeeded` + ); + equal(failed, 0, `${failed} files failed OXC parse`); + }); + + it("AST comparison per file", () => { + for (const file of fixtures.files) { + const fd = getFile(file); + if (!fd.babelAst || !fd.oxcAst) continue; + + const diffs = countDiffs(fd.oxcAst as AnyNode, fd.babelAst as AnyNode); + const diffDetails: string[] = []; + if (diffs > 0) { + diffAst( + fd.oxcAst as AnyNode, + fd.babelAst as AnyNode, + "File", + diffDetails, + 20 + ); + } + + const status = diffs === 0 ? "exact" : `${diffs} diffs`; + console.log(` ${file}: ${status}`); + if (diffDetails.length > 0) { + for (const d of diffDetails.slice(0, 5)) { + console.log(` ${d.slice(0, 120)}`); + } + if (diffDetails.length > 5) { + console.log(` ... and ${diffDetails.length - 5} more`); + } + } + } + ok(true); + }); + }); + + describe("Transformed source comparison", () => { + it("transformed output per file", () => { + let match = 0; + let different = 0; + let failed = 0; + + for (const file of fixtures.files) { + const fd = getFile(file); + const babelSrc = fd.srcTransformedBabel; + const oxcSrc = fd.srcTransformedOxc; + + if (!babelSrc || !oxcSrc) { + failed++; + const reason = !babelSrc + ? "babel transform failed" + : "oxc transform failed"; + console.log(` ${file}: ${reason}`); + if (fd.error) { + console.log(` ${fd.error.message.slice(0, 120)}`); + } + continue; + } + + if (babelSrc === oxcSrc) { + match++; + console.log(` ${file}: MATCH`); + } else { + different++; + // Report first few differing lines + const bLines = babelSrc.split("\n"); + const oLines = oxcSrc.split("\n"); + let diffCount = 0; + const diffSample: string[] = []; + for (let i = 0; i < Math.max(bLines.length, oLines.length); i++) { + if (bLines[i] !== oLines[i]) { + diffCount++; + if (diffSample.length < 3) { + diffSample.push( + ` L${i + 1} babel: ${JSON.stringify(bLines[i])?.slice(0, 80)}\n` + + ` L${i + 1} oxc: ${JSON.stringify(oLines[i])?.slice(0, 80)}` + ); + } + } + } + console.log( + ` ${file}: DIFF (${diffCount} lines differ out of ${Math.max(bLines.length, oLines.length)})` + ); + for (const s of diffSample) console.log(s); + } + } + + const total = match + different; + console.log( + `\nReal-world transformed: ${match}/${total} match, ${different} different, ${failed} failed` + ); + ok(true); + }); + }); +}); diff --git a/incubator/tools-babel/test/sourcemap.test.ts b/incubator/tools-babel/test/sourcemap.test.ts new file mode 100644 index 000000000..0f63c5da9 --- /dev/null +++ b/incubator/tools-babel/test/sourcemap.test.ts @@ -0,0 +1,338 @@ +import { transformFromAstSync } from "@babel/core"; +import type { TransformOptions } from "@babel/core"; +import { transformSync as swcTransformSync } from "@swc/core"; +/** + * Source map tests: verify that when SWC strips TypeScript before OXC parsing, + * passing the SWC source map as inputSourceMap to Babel produces a final + * source map that correctly maps back to the original TypeScript source. + * + * This validates that: + * 1. The Babel AST does NOT need position adjustments for pre-transformed source + * 2. Babel's inputSourceMap composes the SWC map with its own output map + * 3. The composed map traces output positions back to original TS lines + */ +import { ok } from "node:assert/strict"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { makeTransformerArgs } from "../src/options"; +import { oxcParseToAst } from "../src/parse"; +import type { TransformerSettings } from "../src/types"; +import { getRealWorldFixtures } from "./fixtures"; + +// Use a simple VLQ decoder to avoid needing @jridgewell/trace-mapping as a dep. +// We only need originalPositionFor which we implement inline. +type OriginalPosition = { + source: string | null; + line: number | null; + column: number | null; +}; + +type SourceMap = { + sources: string[]; + mappings: string; +}; + +// Simpler approach: parse mappings properly per-segment +function parseSourceMap(map: SourceMap) { + const lines = map.mappings.split(";"); + const segments: { + genLine: number; + genCol: number; + srcIdx: number; + srcLine: number; + srcCol: number; + }[] = []; + let srcIdx = 0; + let srcLine = 0; + let srcCol = 0; + + for (let l = 0; l < lines.length; l++) { + if (!lines[l]) continue; + let genCol = 0; + const parts = lines[l].split(","); + for (const part of parts) { + const values: number[] = []; + let shift = 0; + let value = 0; + for (let i = 0; i < part.length; i++) { + const c = part.charCodeAt(i); + const digit = + c >= 65 && c <= 90 + ? c - 65 + : c >= 97 && c <= 122 + ? c - 97 + 26 + : c >= 48 && c <= 57 + ? c - 48 + 52 + : c === 43 + ? 62 + : c === 47 + ? 63 + : -1; + if (digit < 0) continue; + value += (digit & 31) << shift; + if (digit & 32) { + shift += 5; + } else { + values.push(value & 1 ? -(value >> 1) : value >> 1); + value = 0; + shift = 0; + } + } + if (values.length >= 4) { + genCol += values[0]; + srcIdx += values[1]; + srcLine += values[2]; + srcCol += values[3]; + segments.push({ + genLine: l + 1, + genCol, + srcIdx, + srcLine: srcLine + 1, + srcCol, + }); + } else if (values.length >= 1) { + genCol += values[0]; + } + } + } + return segments; +} + +function originalPositionFor( + map: SourceMap, + pos: { line: number; column: number } +): OriginalPosition { + const segments = parseSourceMap(map); + let best: (typeof segments)[0] | undefined; + for (const seg of segments) { + if (seg.genLine === pos.line && seg.genCol <= pos.column) { + if (!best || seg.genCol > best.genCol) best = seg; + } + } + if (best) { + return { + source: map.sources[best.srcIdx] ?? null, + line: best.srcLine, + column: best.srcCol, + }; + } + return { source: null, line: null, column: null }; +} + +const settings: TransformerSettings = {}; +const fixtures = getRealWorldFixtures(); +const fixtureDir = fixtures.dir; + +/** + * Run the full SWC → OXC → Babel pipeline for a TypeScript file and return + * the final source map alongside the intermediate artifacts. + */ +function transformWithSwcMap(file: string) { + const filename = path.join(fixtureDir, file); + const originalTs = fixtures.getSrc(file); + const isTsx = filename.endsWith(".tsx"); + + // Step 1: SWC strips TypeScript → JS + source map + const swcResult = swcTransformSync(originalTs, { + filename, + sourceFileName: filename, + jsc: { + parser: { syntax: "typescript", tsx: isTsx }, + target: "es2022", + }, + sourceMaps: true, + isModule: true, + }); + const swcJs = swcResult.code; + const swcMap = JSON.parse(swcResult.map!); + + // Step 2: Parse SWC output with OXC (pretend it's a .js file) + const jsFilename = filename.replace(/\.tsx?$/, isTsx ? ".jsx" : ".js"); + const args = makeTransformerArgs( + { + filename: jsFilename, + src: swcJs, + options: { + dev: false, + hot: false, + minify: false, + platform: "ios", + enableBabelRCLookup: true, + enableBabelRuntime: true, + publicPath: "/", + globalPrefix: "", + unstable_transformProfile: "default", + experimentalImportSupport: false, + projectRoot: process.cwd(), + }, + plugins: [], + }, + settings + ); + if (!args) throw new Error(`makeTransformerArgs failed for ${filename}`); + + const oxcAst = oxcParseToAst(args); + if (!oxcAst) throw new Error(`OXC parse failed for ${filename}`); + + // Step 3: Transform with Babel, passing inputSourceMap + const config: TransformOptions = { + ...args.config, + sourceMaps: true, + code: true, + compact: false, + inputSourceMap: swcMap, + }; + + const result = transformFromAstSync(oxcAst, swcJs, config); + if (!result?.map) throw new Error(`Babel transform produced no map`); + + return { + originalTs, + swcJs, + swcMap, + outputCode: result.code!, + outputMap: result.map, + }; +} + +describe("Source maps with SWC pre-transform", () => { + const files = fixtures.getFiles("ts"); + + it("realworld files transform through SWC → OXC → Babel", () => { + let success = 0; + for (const file of files) { + try { + const result = transformWithSwcMap(file); + ok(result.outputCode.length > 0, `${file}: output code is empty`); + ok(result.outputMap.mappings, `${file}: no mappings in source map`); + console.log( + ` ${file}: ${result.originalTs.split("\n").length} TS lines → ${result.swcJs.split("\n").length} JS lines → ${result.outputCode.split("\n").length} output lines` + ); + success++; + } catch (e) { + // Some files (e.g. codegen native components) fail when TS types + // are stripped before Babel — these would skip SWC in production. + console.log( + ` ${file}: skipped (${(e as Error).message.slice(0, 80)})` + ); + } + } + ok(success > 0, "No files could be transformed"); + console.log(` ${success}/${files.length} files transformed successfully`); + }); + + it("source map traces back to original TypeScript source", () => { + let tested = 0; + for (const file of files) { + let result; + try { + result = transformWithSwcMap(file); + } catch { + continue; + } + const { originalTs, outputCode, outputMap } = result; + const tsLines = originalTs.split("\n"); + const outputLines = outputCode.split("\n"); + + // Verify the map points to the original source, not the intermediate JS + ok( + outputMap.sources.some((s: string) => s.includes(file)), + `${file}: source map sources should reference original file, got: ${outputMap.sources}` + ); + + // Trace several output positions back to original TS + const tracer = outputMap; + let traced = 0; + let valid = 0; + + for (let i = 0; i < outputLines.length && traced < 20; i++) { + // Find identifiers/keywords in the output to trace + const match = outputLines[i].match( + /\b(require|exports|function|return|var|const|let)\b/ + ); + if (!match) continue; + + const col = outputLines[i].indexOf(match[1]); + const orig = originalPositionFor(tracer as SourceMap, { + line: i + 1, + column: col, + }); + if (orig.line != null && orig.line > 0) { + traced++; + const origLine = tsLines[orig.line - 1]; + if (origLine != null) { + valid++; + } + } + } + + console.log( + ` ${file}: ${valid}/${traced} traced positions map to valid original lines` + ); + ok(traced > 0, `${file}: could not trace any positions back to original`); + ok( + valid === traced, + `${file}: ${traced - valid} positions mapped to invalid original lines` + ); + tested++; + } + ok(tested > 0, "No files could be tested"); + }); + + it("import/require mappings trace to original import statements", () => { + for (const file of files) { + let result; + try { + result = transformWithSwcMap(file); + } catch { + continue; + } + const { originalTs, outputCode, outputMap } = result; + const tsLines = originalTs.split("\n"); + const outputLines = outputCode.split("\n"); + const tracer = outputMap; + + let importTraced = 0; + let importCorrect = 0; + + for (let i = 0; i < outputLines.length; i++) { + const match = outputLines[i].match(/require\(["']([^"']+)/); + if (!match) continue; + + const col = outputLines[i].indexOf("require"); + const orig = originalPositionFor(tracer as SourceMap, { + line: i + 1, + column: col, + }); + + if (orig.line != null && orig.line > 0) { + importTraced++; + const origLine = tsLines[orig.line - 1] || ""; + // The original line should contain an import or require for the same module + const moduleName = match[1].split("/").pop(); + if ( + origLine.includes("import") || + origLine.includes("require") || + origLine.includes(moduleName!) + ) { + importCorrect++; + } else { + console.log( + ` ${file}: require("${match[1]}") at L${i + 1} → original L${orig.line}: ${origLine.trim().slice(0, 80)}` + ); + } + } + } + + if (importTraced > 0) { + console.log( + ` ${file}: ${importCorrect}/${importTraced} require() calls trace to matching import lines` + ); + ok( + importCorrect / importTraced >= 0.8, + `${file}: only ${importCorrect}/${importTraced} imports traced correctly` + ); + } + } + }); +}); diff --git a/incubator/tools-babel/test/transform-src.test.ts b/incubator/tools-babel/test/transform-src.test.ts new file mode 100644 index 000000000..f953cf320 --- /dev/null +++ b/incubator/tools-babel/test/transform-src.test.ts @@ -0,0 +1,165 @@ +/** + * Compare the generated source from Babel-transformed ASTs, using both + * Babel-parsed and OXC-parsed inputs. The goal is to verify that the + * OXC parse + estree conversion produces equivalent transformed output. + */ +import { ok } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { getFixtures, type FileData } from "./fixtures"; + +const fixtures = getFixtures(); +const fileCache: Record = {}; + +function getFile(file: string): FileData { + if (!fileCache[file]) { + fileCache[file] = fixtures.getFileData(file); + } + return fileCache[file]; +} + +const jsNonCommentFiles = fixtures.getFiles("js-no-comments")!; +const jsCommentFiles = fixtures.getFiles("js-comments")!; +const tsFiles = fixtures.getFiles("ts")!; + +describe("Transformed source comparison: OXC vs Babel", () => { + describe("JS/JSX non-comment fixtures", () => { + it("transformed source matches for most fixtures", () => { + let exact = 0; + let different = 0; + let babelFailed = 0; + let oxcFailed = 0; + const details: string[] = []; + + for (const file of jsNonCommentFiles) { + const fileData = getFile(file); + + const babelSrc = fileData.srcTransformedBabel; + if (!babelSrc) { + if (fileData.error) babelFailed++; + continue; + } + + const oxcSrc = fileData.srcTransformedOxc; + if (!oxcSrc) { + oxcFailed++; + continue; + } + + if (babelSrc === oxcSrc) { + exact++; + } else { + different++; + if (details.length < 15) { + // find first differing line + const babelLines = babelSrc.split("\n"); + const oxcLines = oxcSrc.split("\n"); + for ( + let i = 0; + i < Math.max(babelLines.length, oxcLines.length); + i++ + ) { + if (babelLines[i] !== oxcLines[i]) { + details.push( + `${file} line ${i + 1}:\n babel: ${JSON.stringify(babelLines[i]?.slice(0, 100))}\n oxc: ${JSON.stringify(oxcLines[i]?.slice(0, 100))}` + ); + break; + } + } + } + } + } + + const total = exact + different; + console.log( + `Non-comment JS transformed source: ${exact}/${total} exact match, ${different} different` + ); + if (babelFailed > 0) + console.log(` Babel transform failed: ${babelFailed}`); + if (oxcFailed > 0) console.log(` OXC transform failed: ${oxcFailed}`); + if (details.length > 0) { + console.log(`First mismatches:\n${details.join("\n")}`); + } + + // at least 60% should match + ok( + total === 0 || exact / total >= 0.6, + `Only ${exact}/${total} (${total > 0 ? ((exact / total) * 100).toFixed(0) : 0}%) transformed source matches, expected >= 60%` + ); + }); + }); + + describe("JS/JSX comment fixtures", () => { + it("transformed source comparison", () => { + let exact = 0; + let different = 0; + let failed = 0; + + for (const file of jsCommentFiles) { + const fileData = getFile(file); + const babelSrc = fileData.srcTransformedBabel; + const oxcSrc = fileData.srcTransformedOxc; + if (!babelSrc || !oxcSrc) { + failed++; + continue; + } + if (babelSrc === oxcSrc) exact++; + else different++; + } + + const total = exact + different; + console.log( + `Comment JS transformed source: ${exact}/${total} match, ${different} different, ${failed} failed` + ); + ok(true); + }); + }); + + describe("TS/TSX fixtures", () => { + it("transformed source comparison", () => { + let exact = 0; + let different = 0; + let failed = 0; + const details: string[] = []; + + for (const file of tsFiles) { + const fileData = getFile(file); + const babelSrc = fileData.srcTransformedBabel; + const oxcSrc = fileData.srcTransformedOxc; + if (!babelSrc || !oxcSrc) { + failed++; + continue; + } + if (babelSrc === oxcSrc) { + exact++; + } else { + different++; + if (details.length < 10) { + const babelLines = babelSrc.split("\n"); + const oxcLines = oxcSrc.split("\n"); + for ( + let i = 0; + i < Math.max(babelLines.length, oxcLines.length); + i++ + ) { + if (babelLines[i] !== oxcLines[i]) { + details.push( + `${file} line ${i + 1}:\n babel: ${JSON.stringify(babelLines[i]?.slice(0, 100))}\n oxc: ${JSON.stringify(oxcLines[i]?.slice(0, 100))}` + ); + break; + } + } + } + } + } + + const total = exact + different; + console.log( + `TS transformed source: ${exact}/${total} match, ${different} different, ${failed} failed` + ); + if (details.length > 0) { + console.log(`First mismatches:\n${details.join("\n")}`); + } + ok(true); + }); + }); +}); diff --git a/incubator/tools-babel/tsconfig.json b/incubator/tools-babel/tsconfig.json new file mode 100644 index 000000000..a23b858b9 --- /dev/null +++ b/incubator/tools-babel/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rnx-kit/tsconfig/tsconfig.nodenext.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/incubator/tools-performance/src/table.ts b/incubator/tools-performance/src/table.ts index 38d8300c6..2fcd9a52d 100644 --- a/incubator/tools-performance/src/table.ts +++ b/incubator/tools-performance/src/table.ts @@ -219,7 +219,10 @@ function drawLine( line += "─".repeat(colData[i].width + 2); line += i === colData.length - 1 ? seg[2] : seg[1]; } - return line + "\n"; + if (type !== "bottom") { + line += "\n"; + } + return line; } /** diff --git a/package.json b/package.json index 019d24f96..026335ede 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,12 @@ "@rnx-kit/jest-preset" ] }, + "incubator/tools-babel": { + "entry": [ + "src/**/index.ts", + "test/**/*.ts" + ] + }, "incubator/tools-typescript": { "entry": [ "test/**/*.test.ts" diff --git a/yarn.lock b/yarn.lock index 780d6cc30..1f059a889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -213,7 +213,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": version: 7.29.1 resolution: "@babel/generator@npm:7.29.1" dependencies: @@ -1864,7 +1864,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.1.0, @emnapi/core@npm:^1.7.1": +"@emnapi/core@npm:^1.1.0": version: 1.8.1 resolution: "@emnapi/core@npm:1.8.1" dependencies: @@ -1874,7 +1874,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.1.0, @emnapi/runtime@npm:^1.7.1": +"@emnapi/runtime@npm:^1.1.0": version: 1.8.1 resolution: "@emnapi/runtime@npm:1.8.1" dependencies: @@ -3015,14 +3015,15 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.1": - version: 1.1.1 - resolution: "@napi-rs/wasm-runtime@npm:1.1.1" +"@napi-rs/wasm-runtime@npm:^1.1.1, @napi-rs/wasm-runtime@npm:^1.1.2": + version: 1.1.3 + resolution: "@napi-rs/wasm-runtime@npm:1.1.3" dependencies: - "@emnapi/core": "npm:^1.7.1" - "@emnapi/runtime": "npm:^1.7.1" "@tybys/wasm-util": "npm:^0.10.1" - checksum: 10c0/04d57b67e80736e41fe44674a011878db0a8ad893f4d44abb9d3608debb7c174224cba2796ed5b0c1d367368159f3ca6be45f1c59222f70e32ddc880f803d447 + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + checksum: 10c0/745bb32a023b95095a18d93658bf4564403c2283ca0500a043afcf566ac6082bd0611792f14636276bab07dc2ce6d862591c8aabddae02ec697245b05bc6f144 languageName: node linkType: hard @@ -3291,6 +3292,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-android-arm-eabi@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-android-arm-eabi@npm:0.123.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@oxc-parser/binding-android-arm64@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-android-arm64@npm:0.120.0" @@ -3298,6 +3306,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-android-arm64@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-android-arm64@npm:0.123.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@oxc-parser/binding-darwin-arm64@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-darwin-arm64@npm:0.120.0" @@ -3305,6 +3320,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-darwin-arm64@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-darwin-arm64@npm:0.123.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@oxc-parser/binding-darwin-x64@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-darwin-x64@npm:0.120.0" @@ -3312,6 +3334,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-darwin-x64@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-darwin-x64@npm:0.123.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@oxc-parser/binding-freebsd-x64@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-freebsd-x64@npm:0.120.0" @@ -3319,6 +3348,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-freebsd-x64@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-freebsd-x64@npm:0.123.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.120.0" @@ -3326,6 +3362,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.123.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@oxc-parser/binding-linux-arm-musleabihf@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.120.0" @@ -3333,6 +3376,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-arm-musleabihf@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.123.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@oxc-parser/binding-linux-arm64-gnu@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.120.0" @@ -3340,6 +3390,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-arm64-gnu@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.123.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@oxc-parser/binding-linux-arm64-musl@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.120.0" @@ -3347,6 +3404,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-arm64-musl@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.123.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@oxc-parser/binding-linux-ppc64-gnu@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.120.0" @@ -3354,6 +3418,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-ppc64-gnu@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-ppc64-gnu@npm:0.123.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@oxc-parser/binding-linux-riscv64-gnu@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.120.0" @@ -3361,6 +3432,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-riscv64-gnu@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.123.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@oxc-parser/binding-linux-riscv64-musl@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.120.0" @@ -3368,6 +3446,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-riscv64-musl@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-riscv64-musl@npm:0.123.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@oxc-parser/binding-linux-s390x-gnu@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.120.0" @@ -3375,6 +3460,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-s390x-gnu@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.123.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@oxc-parser/binding-linux-x64-gnu@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.120.0" @@ -3382,6 +3474,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-x64-gnu@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.123.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@oxc-parser/binding-linux-x64-musl@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.120.0" @@ -3389,6 +3488,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-linux-x64-musl@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.123.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@oxc-parser/binding-openharmony-arm64@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.120.0" @@ -3396,6 +3502,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-openharmony-arm64@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-openharmony-arm64@npm:0.123.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@oxc-parser/binding-wasm32-wasi@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.120.0" @@ -3405,6 +3518,15 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-wasm32-wasi@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.123.0" + dependencies: + "@napi-rs/wasm-runtime": "npm:^1.1.2" + conditions: cpu=wasm32 + languageName: node + linkType: hard + "@oxc-parser/binding-win32-arm64-msvc@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.120.0" @@ -3412,6 +3534,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-win32-arm64-msvc@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.123.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@oxc-parser/binding-win32-ia32-msvc@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.120.0" @@ -3419,6 +3548,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-win32-ia32-msvc@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-win32-ia32-msvc@npm:0.123.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@oxc-parser/binding-win32-x64-msvc@npm:0.120.0": version: 0.120.0 resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.120.0" @@ -3426,6 +3562,13 @@ __metadata: languageName: node linkType: hard +"@oxc-parser/binding-win32-x64-msvc@npm:0.123.0": + version: 0.123.0 + resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.123.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@oxc-project/types@npm:^0.120.0": version: 0.120.0 resolution: "@oxc-project/types@npm:0.120.0" @@ -3433,6 +3576,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:^0.123.0": + version: 0.123.0 + resolution: "@oxc-project/types@npm:0.123.0" + checksum: 10c0/7f71f9fa38796e6e5431390c213ec9626a3972feec07b513c513828bbfba5f6d908b04e8c679ae2b30b49cc1dee2dc0b2f1012f38ed1cb9e54bfeba09119f36d + languageName: node + linkType: hard + "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1": version: 11.19.1 resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.19.1" @@ -5846,7 +5996,7 @@ __metadata: languageName: unknown linkType: soft -"@rnx-kit/reporter@workspace:incubator/reporter": +"@rnx-kit/reporter@npm:*, @rnx-kit/reporter@workspace:incubator/reporter": version: 0.0.0-use.local resolution: "@rnx-kit/reporter@workspace:incubator/reporter" dependencies: @@ -6040,7 +6190,7 @@ __metadata: languageName: unknown linkType: soft -"@rnx-kit/test-fixtures@workspace:incubator/test-fixtures": +"@rnx-kit/test-fixtures@npm:*, @rnx-kit/test-fixtures@workspace:incubator/test-fixtures": version: 0.0.0-use.local resolution: "@rnx-kit/test-fixtures@workspace:incubator/test-fixtures" dependencies: @@ -6092,6 +6242,30 @@ __metadata: languageName: unknown linkType: soft +"@rnx-kit/tools-babel@workspace:incubator/tools-babel": + version: 0.0.0-use.local + resolution: "@rnx-kit/tools-babel@workspace:incubator/tools-babel" + dependencies: + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.0" + "@react-native/babel-preset": "npm:^0.83.0" + "@rnx-kit/reporter": "npm:*" + "@rnx-kit/scripts": "npm:*" + "@rnx-kit/test-fixtures": "npm:*" + "@rnx-kit/tools-performance": "npm:^0.1.0" + "@rnx-kit/tsconfig": "npm:*" + "@swc/core": "npm:^1.15.24" + "@types/babel__core": "npm:^7.20.0" + "@types/babel__generator": "npm:^7.20.0" + hermes-parser: "npm:^0.34.0" + metro-babel-transformer: "npm:^0.83.1" + oxc-parser: "npm:^0.123.0" + peerDependencies: + "@babel/core": "*" + "@react-native/babel-preset": "*" + languageName: unknown + linkType: soft + "@rnx-kit/tools-filesystem@npm:^0.2.0, @rnx-kit/tools-filesystem@workspace:packages/tools-filesystem": version: 0.0.0-use.local resolution: "@rnx-kit/tools-filesystem@workspace:packages/tools-filesystem" @@ -6142,7 +6316,7 @@ __metadata: languageName: unknown linkType: soft -"@rnx-kit/tools-performance@workspace:incubator/tools-performance": +"@rnx-kit/tools-performance@npm:^0.1.0, @rnx-kit/tools-performance@workspace:incubator/tools-performance": version: 0.0.0-use.local resolution: "@rnx-kit/tools-performance@workspace:incubator/tools-performance" dependencies: @@ -6552,6 +6726,158 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-darwin-arm64@npm:1.15.24" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-darwin-x64@npm:1.15.24" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.24" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.24" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.24" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-ppc64-gnu@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-ppc64-gnu@npm:1.15.24" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-s390x-gnu@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-s390x-gnu@npm:1.15.24" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.24" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-linux-x64-musl@npm:1.15.24" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.24" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.24" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.15.24": + version: 1.15.24 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.24" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.15.24": + version: 1.15.24 + resolution: "@swc/core@npm:1.15.24" + dependencies: + "@swc/core-darwin-arm64": "npm:1.15.24" + "@swc/core-darwin-x64": "npm:1.15.24" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.24" + "@swc/core-linux-arm64-gnu": "npm:1.15.24" + "@swc/core-linux-arm64-musl": "npm:1.15.24" + "@swc/core-linux-ppc64-gnu": "npm:1.15.24" + "@swc/core-linux-s390x-gnu": "npm:1.15.24" + "@swc/core-linux-x64-gnu": "npm:1.15.24" + "@swc/core-linux-x64-musl": "npm:1.15.24" + "@swc/core-win32-arm64-msvc": "npm:1.15.24" + "@swc/core-win32-ia32-msvc": "npm:1.15.24" + "@swc/core-win32-x64-msvc": "npm:1.15.24" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.26" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-ppc64-gnu": + optional: true + "@swc/core-linux-s390x-gnu": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10c0/dfa963d88c2044517b483dfe2104744018c59c8524b3d82c61a15132558ba03e73d7b8ee79824509c7ae5f962a5dcd27b6e5acbeec66978e0fc757bec2b9603e + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356 + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.26": + version: 0.1.26 + resolution: "@swc/types@npm:0.1.26" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10c0/8449341e8bbff81c14e9918c25421143cf605dff20f70f048847e1f7cede396f8dd73903cbef331a809b4a8e15d0db374a5f6809003e7b440f93df1dd4934d28 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -6615,7 +6941,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:*, @types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": +"@types/babel__core@npm:*, @types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.0": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -6628,12 +6954,12 @@ __metadata: languageName: node linkType: hard -"@types/babel__generator@npm:*": - version: 7.6.4 - resolution: "@types/babel__generator@npm:7.6.4" +"@types/babel__generator@npm:*, @types/babel__generator@npm:^7.20.0": + version: 7.27.0 + resolution: "@types/babel__generator@npm:7.27.0" dependencies: "@babel/types": "npm:^7.0.0" - checksum: 10c0/e0051b450e4ba2df0a7e386f08df902a4e920f6f8d6f185d69ddbe9b0e2e2d3ae434bb51e437bc0fca2a9a0f5dc4ca44d3a1941ef75e74371e8be5bf64416fe4 + checksum: 10c0/9f9e959a8792df208a9d048092fda7e1858bddc95c6314857a8211a99e20e6830bdeb572e3587ae8be5429e37f2a96fcf222a9f53ad232f5537764c9e13a2bbd languageName: node linkType: hard @@ -11267,6 +11593,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.34.0": + version: 0.34.0 + resolution: "hermes-estree@npm:0.34.0" + checksum: 10c0/bd4ad520838c69aa79887230a2030fe1e07d0826389112e2c23a8b18494f9f2fa6b1639f413ad978f3468daea66903869188481f9500aaa1fb79ed6266afb744 + languageName: node + linkType: hard + "hermes-parser@npm:0.25.1": version: 0.25.1 resolution: "hermes-parser@npm:0.25.1" @@ -11294,6 +11627,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:^0.34.0": + version: 0.34.0 + resolution: "hermes-parser@npm:0.34.0" + dependencies: + hermes-estree: "npm:0.34.0" + checksum: 10c0/e20657a21ebc3187f53780f5f2c5dd7434f4371979d05b016ff06306b6db63f9d2575ee60c63e9e7d831dd0f592542193a50a6e8397678d4a312fc5373bbe382 + languageName: node + linkType: hard + "hpagent@npm:^1.2.0": version: 1.2.0 resolution: "hpagent@npm:1.2.0" @@ -14941,6 +15283,76 @@ __metadata: languageName: node linkType: hard +"oxc-parser@npm:^0.123.0": + version: 0.123.0 + resolution: "oxc-parser@npm:0.123.0" + dependencies: + "@oxc-parser/binding-android-arm-eabi": "npm:0.123.0" + "@oxc-parser/binding-android-arm64": "npm:0.123.0" + "@oxc-parser/binding-darwin-arm64": "npm:0.123.0" + "@oxc-parser/binding-darwin-x64": "npm:0.123.0" + "@oxc-parser/binding-freebsd-x64": "npm:0.123.0" + "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.123.0" + "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.123.0" + "@oxc-parser/binding-linux-arm64-gnu": "npm:0.123.0" + "@oxc-parser/binding-linux-arm64-musl": "npm:0.123.0" + "@oxc-parser/binding-linux-ppc64-gnu": "npm:0.123.0" + "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.123.0" + "@oxc-parser/binding-linux-riscv64-musl": "npm:0.123.0" + "@oxc-parser/binding-linux-s390x-gnu": "npm:0.123.0" + "@oxc-parser/binding-linux-x64-gnu": "npm:0.123.0" + "@oxc-parser/binding-linux-x64-musl": "npm:0.123.0" + "@oxc-parser/binding-openharmony-arm64": "npm:0.123.0" + "@oxc-parser/binding-wasm32-wasi": "npm:0.123.0" + "@oxc-parser/binding-win32-arm64-msvc": "npm:0.123.0" + "@oxc-parser/binding-win32-ia32-msvc": "npm:0.123.0" + "@oxc-parser/binding-win32-x64-msvc": "npm:0.123.0" + "@oxc-project/types": "npm:^0.123.0" + dependenciesMeta: + "@oxc-parser/binding-android-arm-eabi": + optional: true + "@oxc-parser/binding-android-arm64": + optional: true + "@oxc-parser/binding-darwin-arm64": + optional: true + "@oxc-parser/binding-darwin-x64": + optional: true + "@oxc-parser/binding-freebsd-x64": + optional: true + "@oxc-parser/binding-linux-arm-gnueabihf": + optional: true + "@oxc-parser/binding-linux-arm-musleabihf": + optional: true + "@oxc-parser/binding-linux-arm64-gnu": + optional: true + "@oxc-parser/binding-linux-arm64-musl": + optional: true + "@oxc-parser/binding-linux-ppc64-gnu": + optional: true + "@oxc-parser/binding-linux-riscv64-gnu": + optional: true + "@oxc-parser/binding-linux-riscv64-musl": + optional: true + "@oxc-parser/binding-linux-s390x-gnu": + optional: true + "@oxc-parser/binding-linux-x64-gnu": + optional: true + "@oxc-parser/binding-linux-x64-musl": + optional: true + "@oxc-parser/binding-openharmony-arm64": + optional: true + "@oxc-parser/binding-wasm32-wasi": + optional: true + "@oxc-parser/binding-win32-arm64-msvc": + optional: true + "@oxc-parser/binding-win32-ia32-msvc": + optional: true + "@oxc-parser/binding-win32-x64-msvc": + optional: true + checksum: 10c0/e0149bd38e1ebc6faf5b1126b3c43e377f7db80f0f5ec8f69a273864de596bc808efd206d7b7c061aa522b038565af8779d6b77c94e7c6d11b6608cae78f7638 + languageName: node + linkType: hard + "oxc-resolver@npm:^11.0.0, oxc-resolver@npm:^11.19.1": version: 11.19.1 resolution: "oxc-resolver@npm:11.19.1"