# `OXC`
[🔗](https://github.com/elixir-volt/oxc_ex/blob/v0.10.0/lib/oxc.ex#L1)

Elixir bindings for the [OXC](https://oxc.rs) JavaScript toolchain.

Provides fast JavaScript and TypeScript parsing, transformation, and
minification via Rust NIFs. The file extension determines the dialect —
`.js`, `.jsx`, `.ts`, `.tsx`.

    iex> {:ok, ast} = OXC.parse("const x = 1 + 2", "test.js")
    iex> ast.type
    :program

    iex> {:ok, js} = OXC.transform("const x: number = 42", "test.ts")
    iex> js
    "const x = 42;\n"

AST nodes are maps with atom keys, following the ESTree specification.
The `:type` and `:kind` field values are snake_case atoms
(e.g. `:import_declaration`, `:variable_declaration`).

# `ast`

```elixir
@type ast() :: %{:type =&gt; atom(), optional(atom()) =&gt; any()}
```

# `bundle_result`

```elixir
@type bundle_result() ::
  {:ok, String.t() | code_with_sourcemap()} | {:error, [error()]}
```

# `code_with_sourcemap`

```elixir
@type code_with_sourcemap() :: %{code: String.t(), sourcemap: String.t()}
```

# `error`

```elixir
@type error() :: %{message: String.t()}
```

# `parse_result`

```elixir
@type parse_result() :: {:ok, ast()} | {:error, [error()]}
```

# `patch`

```elixir
@type patch() :: %{
  start: non_neg_integer(),
  end: non_neg_integer(),
  change: String.t()
}
```

# `transform_result`

```elixir
@type transform_result() ::
  {:ok, String.t() | code_with_sourcemap()} | {:error, [error()]}
```

# `bind`

```elixir
@spec bind(
  ast(),
  keyword()
) :: ast()
```

Substitute `$placeholders` in an AST with provided values.

Walks the AST and replaces any identifier node whose name starts with `$`
with the corresponding value from `bindings`.

Binding values can be:
  * A string — replaced as an identifier name
  * `{:literal, value}` — replaced with a literal node (string, number,
    boolean, nil, map, or list — maps and lists are converted recursively
    into JS object/array expressions)
  * `{:expr, code}` — parsed as a JavaScript expression
  * A map with `:type` — spliced as a raw AST node

## Examples

    iex> {:ok, ast} = OXC.parse("const x = $value", "t.js")
    iex> ast = OXC.bind(ast, value: {:literal, 42})
    iex> OXC.codegen!(ast) =~ "const x = 42"
    true

    iex> {:ok, ast} = OXC.parse("const $name = 1", "t.js")
    iex> ast = OXC.bind(ast, name: "myVar")
    iex> OXC.codegen!(ast) =~ "const myVar = 1"
    true

# `bundle`

```elixir
@spec bundle(
  [{String.t(), String.t()}],
  keyword()
) :: bundle_result()
```

Bundle multiple TypeScript/JavaScript modules into a single IIFE script.

Takes a list of `{filename, source}` tuples representing a virtual project.
Modules can import each other via relative paths and are bundled into a
single IIFE script.

## Options

  * `:entry` — entry module filename from `files` (required), for example
    `"main.ts"`
  * `:format` — output format: `:iife` (default), `:esm`, or `:cjs`
  * `:minify` — minify the output (default: `false`)
  * `:treeshake` — enable tree-shaking to remove unused exports (default: `false`)
  * `:banner` — string to prepend before the IIFE (e.g. `"/* v1.0 */"`)
  * `:footer` — string to append after the IIFE
  * `:preamble` — code to inject at the top of the IIFE function body,
    before any bundled modules (e.g. `"const { ref } = Vue;"`)
  * `:external` — list of bare specifiers to treat as external and preserve
    as `import` statements in the output (e.g. `["react", "scheduler"]`).
    Bare specifiers from ESM imports are auto-detected as external;
    use this for additional specifiers the auto-detection misses.
  * `:define` — compile-time replacements, map of `%{"process.env.NODE_ENV" => ~s("production")}`
  * `:sourcemap` — generate a source map (default: `false`). When `true`,
    returns `%{code: String.t(), sourcemap: String.t()}` instead of a plain string.
  * `:drop_console` — remove `console.*` calls during minification (default: `false`)
  * `:jsx` — JSX runtime, `:automatic` (default) or `:classic`
  * `:jsx_factory` — function for classic JSX (default: `"React.createElement"`)
  * `:jsx_fragment` — fragment for classic JSX (default: `"React.Fragment"`)
  * `:import_source` — JSX import source (e.g. `"vue"`, `"preact"`)
  * `:target` — downlevel target (e.g. `"es2019"`, `"chrome80"`)

## Examples

    iex> files = [
    ...>   {"event.ts", "export class Event { type: string; constructor(type: string) { this.type = type } }"},
    ...>   {"target.ts", "import { Event } from './event'\nexport class Target extends Event {}"}
    ...> ]
    iex> {:ok, js} = OXC.bundle(files, entry: "target.ts")
    iex> String.contains?(js, "Event")
    true
    iex> String.contains?(js, "Target")
    true
    iex> String.contains?(js, "import ")
    false

# `bundle!`

```elixir
@spec bundle!(
  [{String.t(), String.t()}],
  keyword()
) :: String.t() | code_with_sourcemap()
```

Like `bundle/2` but raises on errors.

# `codegen`

```elixir
@spec codegen(ast()) :: {:ok, String.t()} | {:error, [error()]}
```

Generate JavaScript source code from an AST map.

Takes an ESTree AST (as returned by `parse/2` or constructed manually)
and produces formatted JavaScript source code using OXC's code generator.

Handles operator precedence, indentation, and semicolon insertion.

## Examples

    iex> ast = OXC.parse!("const x = 1 + 2", "test.js")
    iex> {:ok, js} = OXC.codegen(ast)
    iex> js =~ "const x = 1 + 2"
    true

    iex> ast = %{type: :program, body: [
    ...>   %{type: :variable_declaration, kind: :const, declarations: [
    ...>     %{type: :variable_declarator,
    ...>       id: %{type: :identifier, name: "x"},
    ...>       init: %{type: :literal, value: 42}}
    ...>   ]}
    ...> ]}
    iex> {:ok, js} = OXC.codegen(ast)
    iex> js =~ "const x = 42"
    true

# `codegen!`

```elixir
@spec codegen!(ast()) :: String.t()
```

Like `codegen/1` but raises on errors.

# `collect`

```elixir
@spec collect(ast(), (map() -&gt; {:keep, any()} | :skip)) :: [any()]
```

Collect AST nodes that match a filter function.

The function receives each node (map with `:type` key) and should return
`{:keep, value}` to include it in results, or `:skip` to exclude it.

## Examples

    iex> {:ok, ast} = OXC.parse("const x = y + z", "test.js")
    iex> OXC.collect(ast, fn
    ...>   %{type: :identifier, name: name} -> {:keep, name}
    ...>   _ -> :skip
    ...> end)
    ["x", "y", "z"]

# `collect_imports`

```elixir
@spec collect_imports(String.t(), String.t()) ::
  {:ok,
   [
     %{
       specifier: String.t(),
       type: :static | :dynamic,
       kind: :import | :export | :export_all,
       start: non_neg_integer(),
       end: non_neg_integer()
     }
   ]}
  | {:error, [error()]}
```

Analyze imports with type information.

Returns `{:ok, list}` where each element is a map with:
  * `:specifier` — the import source string (e.g. `"vue"`, `"./utils"`)
  * `:type` — `:static` or `:dynamic`
  * `:kind` — `:import`, `:export`, or `:export_all`
  * `:start` — byte offset of the specifier string literal (including quote)
  * `:end` — byte offset of the end of the specifier string literal

Type-only imports/exports (`import type { ... }`, `export type { ... }`)
are excluded.

## Examples

    iex> source = "import { ref } from 'vue'\nexport { foo } from './bar'\nimport('./lazy')"
    iex> {:ok, imports} = OXC.collect_imports(source, "test.js")
    iex> Enum.map(imports, & &1.specifier)
    ["vue", "./bar", "./lazy"]
    iex> Enum.map(imports, & &1.type)
    [:static, :static, :dynamic]
    iex> Enum.map(imports, & &1.kind)
    [:import, :export, :import]

# `collect_imports!`

```elixir
@spec collect_imports!(String.t(), String.t()) :: [map()]
```

Like `collect_imports/2` but raises on errors.

# `imports`

```elixir
@spec imports(String.t(), String.t()) :: {:ok, [String.t()]} | {:error, [error()]}
```

Extract import specifiers from JavaScript/TypeScript source.

Faster than `parse/2` + `collect/2` — skips full AST serialization
and returns only the import source strings. Type-only imports
(`import type { ... }`) are excluded.

## Examples

    iex> {:ok, imports} = OXC.imports("import { ref } from 'vue'\nimport type { Ref } from 'vue'", "test.ts")
    iex> imports
    ["vue"]

# `imports!`

```elixir
@spec imports!(String.t(), String.t()) :: [String.t()]
```

Like `imports/2` but raises on errors.

# `minify`

```elixir
@spec minify(String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, [error()]}
```

Minify JavaScript source code.

Applies dead code elimination, constant folding, and whitespace removal.
Optionally mangles variable names for smaller output.

## Options

  * `:mangle` — rename variables for shorter names (default: `true`)

## Examples

    iex> {:ok, min} = OXC.minify("if (false) { x() } y();", "test.js")
    iex> min =~ "y()"
    true
    iex> min =~ "x()"
    false

# `minify!`

```elixir
@spec minify!(String.t(), String.t(), keyword()) :: String.t()
```

Like `minify/3` but raises on errors.

## Examples

    iex> min = OXC.minify!("const x = 1 + 2;", "test.js")
    iex> is_binary(min)
    true

# `parse`

```elixir
@spec parse(String.t(), String.t()) :: parse_result()
```

Parse JavaScript or TypeScript source code into an ESTree AST.

The filename extension determines the dialect:
- `.js` — JavaScript
- `.jsx` — JavaScript with JSX
- `.ts` — TypeScript
- `.tsx` — TypeScript with JSX

Returns `{:ok, ast}` where `ast` is a map with atom keys, or
`{:error, errors}` with a list of parse error maps.

## Examples

    iex> {:ok, ast} = OXC.parse("const x = 1", "test.js")
    iex> [decl] = ast.body
    iex> decl.type
    :variable_declaration

    iex> {:error, [%{message: msg} | _]} = OXC.parse("const = ;", "bad.js")
    iex> is_binary(msg)
    true

# `parse!`

```elixir
@spec parse!(String.t(), String.t()) :: ast()
```

Like `parse/2` but raises on parse errors.

## Examples

    iex> ast = OXC.parse!("const x = 1", "test.js")
    iex> ast.type
    :program

# `patch_string`

```elixir
@spec patch_string(String.t(), [patch()]) :: String.t()
```

Apply patches to source code, like `Sourceror.patch_string/2`.

Each patch is a map with `:start` (byte offset), `:end` (byte offset),
and `:change` (replacement string). Patches are applied in reverse
offset order so that earlier patches don't shift later offsets.

When multiple patches target the same `{start, end}` range, only the
first one is applied and duplicates are silently dropped.

Use with `postwalk/3` to collect patches from the AST, then apply
them to the original source string.

## Examples

    iex> OXC.patch_string("hello world", [%{start: 6, end: 11, change: "elixir"}])
    "hello elixir"

    iex> source = "import { ref } from 'vue'"
    iex> OXC.patch_string(source, [%{start: 20, end: 25, change: "'/@vendor/vue.js'"}])
    "import { ref } from '/@vendor/vue.js'"

# `postwalk`

```elixir
@spec postwalk(ast() | [ast()], (map() -&gt; map())) :: map() | [map()]
```

Depth-first post-order traversal, like `Macro.postwalk/2`.

Visits every AST node (map with a `:type` key). Children are visited
first, then the node itself. The callback returns the (possibly modified)
node.

Accepts a single AST node or a list of nodes.

## Examples

    iex> {:ok, ast} = OXC.parse("const x = 1", "test.js")
    iex> OXC.postwalk(ast, fn
    ...>   %{type: :identifier, name: "x"} = node -> %{node | name: "y"}
    ...>   node -> node
    ...> end)
    iex> :ok
    :ok

# `postwalk`

```elixir
@spec postwalk(ast() | [ast()], acc, (map(), acc -&gt; {map(), acc})) ::
  {map() | [map()], acc}
when acc: term()
```

Depth-first post-order traversal with accumulator, like `Macro.postwalk/3`.

The callback receives each AST node and the accumulator, and must return
`{node, acc}`. Use this to collect data while traversing.

Accepts a single AST node or a list of nodes.

## Examples

    iex> source = "import { ref } from 'vue'\nimport a from './utils'"
    iex> {:ok, ast} = OXC.parse(source, "test.ts")
    iex> {_ast, patches} = OXC.postwalk(ast, [], fn
    ...>   %{type: :import_declaration, source: %{value: "vue"} = src} = node, patches ->
    ...>     {node, [%{start: src.start, end: src.end, change: "'/@vendor/vue.js'"} | patches]}
    ...>   node, patches ->
    ...>     {node, patches}
    ...> end)
    iex> OXC.patch_string(source, patches)
    "import { ref } from '/@vendor/vue.js'\nimport a from './utils'"

# `rewrite_specifiers`

```elixir
@spec rewrite_specifiers(String.t(), String.t(), (String.t() -&gt;
                                              {:rewrite, String.t()} | :keep)) ::
  {:ok, String.t()} | {:error, [error()]}
```

Rewrite import/export specifiers in a single pass.

Parses the source, finds all import/export declarations
(ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration,
and dynamic ImportExpression), and calls `fun` with each specifier string.

The callback returns:
  * `{:rewrite, new_specifier}` — replace the specifier
  * `:keep` — leave unchanged

Returns `{:ok, patched_source}` or `{:error, errors}`.

## Examples

    iex> source = "import { ref } from 'vue'\nimport a from './utils'"
    iex> {:ok, result} = OXC.rewrite_specifiers(source, "test.js", fn
    ...>   "vue" -> {:rewrite, "/@vendor/vue.js"}
    ...>   _ -> :keep
    ...> end)
    iex> result
    "import { ref } from '/@vendor/vue.js'\nimport a from './utils'"

# `rewrite_specifiers!`

```elixir
@spec rewrite_specifiers!(String.t(), String.t(), (String.t() -&gt;
                                               {:rewrite, String.t()} | :keep)) ::
  String.t()
```

Like `rewrite_specifiers/3` but raises on errors.

# `splice`

```elixir
@spec splice(ast(), atom(), ast() | String.t() | [ast() | String.t()]) :: ast()
```

Replace `$placeholder` statements, properties, or elements with a list of nodes.

Finds expression statements, shorthand object properties, or array elements
whose identifier name starts with `$` and replaces them with the provided
nodes. Accepts a single item or a list. Strings are auto-parsed as JS.

## Examples

    iex> {:ok, ast} = OXC.parse("function f() { $body }", "t.js")
    iex> ast = OXC.splice(ast, :body, ["const x = 1;", "return x;"])
    iex> js = OXC.codegen!(ast)
    iex> js =~ "const x = 1" and js =~ "return x"
    true

    iex> {:ok, ast} = OXC.parse("const obj = {a: 1, $rest}", "t.js")
    iex> ast = OXC.splice(ast, :rest, ["b: 2", "c: 3"])
    iex> js = OXC.codegen!(ast)
    iex> js =~ "b: 2" and js =~ "c: 3"
    true

# `transform`

```elixir
@spec transform(String.t(), String.t(), keyword()) :: transform_result()
```

Transform TypeScript/JSX source code into plain JavaScript.

Strips type annotations, transforms JSX, and lowers syntax features.
The filename extension determines the source dialect.

## Options

  * `:jsx` — JSX runtime, `:automatic` (default) or `:classic`
  * `:jsx_factory` — function for classic JSX (default: `"React.createElement"`)
  * `:jsx_fragment` — fragment for classic JSX (default: `"React.Fragment"`)
  * `:import_source` — JSX import source (e.g. `"vue"`, `"preact"`)
  * `:target` — downlevel target (e.g. `"es2019"`, `"chrome80"`)
  * `:sourcemap` — generate a source map (default: `false`). When `true`,
    returns `%{code: String.t(), sourcemap: String.t()}` instead of a plain string.

## Examples

    iex> {:ok, js} = OXC.transform("const x: number = 42", "test.ts")
    iex> js
    "const x = 42;\n"

    iex> {:ok, js} = OXC.transform("<div />", "c.jsx", jsx: :classic)
    iex> js =~ "createElement"
    true

# `transform!`

```elixir
@spec transform!(String.t(), String.t(), keyword()) ::
  String.t() | code_with_sourcemap()
```

Like `transform/3` but raises on errors.

## Examples

    iex> OXC.transform!("const x: number = 42", "test.ts")
    "const x = 42;\n"

# `transform_many`

```elixir
@spec transform_many(
  [{String.t(), String.t()}],
  keyword()
) :: [transform_result()]
```

Transform multiple source files in parallel using a Rust thread pool.

Accepts a list of `{source, filename}` tuples and shared options.
Returns a list of results in the same order, each being
`{:ok, code}`, `{:ok, %{code: ..., sourcemap: ...}}`, or `{:error, errors}`.

Significantly faster than calling `transform/3` sequentially for many files,
since work is distributed across OS threads without BEAM scheduling overhead.

## Examples

    iex> results = OXC.transform_many([{"const x: number = 1", "a.ts"}, {"const y: number = 2", "b.ts"}])
    iex> length(results)
    2
    iex> {:ok, code} = hd(results)
    iex> code =~ "const x = 1"
    true

# `valid?`

```elixir
@spec valid?(String.t(), String.t()) :: boolean()
```

Check if source code is syntactically valid.

Faster than `parse/2` — skips AST serialization.

## Examples

    iex> OXC.valid?("const x = 1", "test.js")
    true

    iex> OXC.valid?("const = ;", "bad.js")
    false

# `walk`

```elixir
@spec walk(ast() | [ast()], (map() -&gt; any())) :: :ok
```

Walk an AST tree, calling `fun` on every node (any map with a `:type` key).

Descends into all map values and list elements to reach nested AST
nodes, including maps without a `:type` key (which are skipped for
the callback but still traversed).

## Examples

    iex> {:ok, ast} = OXC.parse("const x = 1", "test.js")
    iex> OXC.walk(ast, fn
    ...>   %{type: :identifier, name: name} -> send(self(), {:id, name})
    ...>   _ -> :ok
    ...> end)
    iex> receive do {:id, name} -> name end
    "x"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
