Elixir bindings for the OXC 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).
Summary
Functions
Substitute $placeholders in an AST with provided values.
Bundle multiple TypeScript/JavaScript modules into a single IIFE script.
Like bundle/2 but raises on errors.
Generate JavaScript source code from an AST map.
Like codegen/1 but raises on errors.
Collect AST nodes that match a filter function.
Analyze imports with type information.
Like collect_imports/2 but raises on errors.
Extract import specifiers from JavaScript/TypeScript source.
Like imports/2 but raises on errors.
Minify JavaScript source code.
Like minify/3 but raises on errors.
Parse JavaScript or TypeScript source code into an ESTree AST.
Like parse/2 but raises on parse errors.
Apply patches to source code, like Sourceror.patch_string/2.
Depth-first post-order traversal, like Macro.postwalk/2.
Depth-first post-order traversal with accumulator, like Macro.postwalk/3.
Rewrite import/export specifiers in a single pass.
Like rewrite_specifiers/3 but raises on errors.
Replace $placeholder statements, properties, or elements with a list of nodes.
Transform TypeScript/JSX source code into plain JavaScript.
Like transform/3 but raises on errors.
Transform multiple source files in parallel using a Rust thread pool.
Check if source code is syntactically valid.
Walk an AST tree, calling fun on every node (any map with a :type key).
Types
@type bundle_result() :: {:ok, String.t() | code_with_sourcemap()} | {:error, [error()]}
@type error() :: %{message: String.t()}
@type patch() :: %{ start: non_neg_integer(), end: non_neg_integer(), change: String.t() }
@type transform_result() :: {:ok, String.t() | code_with_sourcemap()} | {:error, [error()]}
Functions
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
@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 fromfiles(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 asimportstatements 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). Whentrue, returns%{code: String.t(), sourcemap: String.t()}instead of a plain string.:drop_console— removeconsole.*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
@spec bundle!( [{String.t(), String.t()}], keyword() ) :: String.t() | code_with_sourcemap()
Like bundle/2 but raises on errors.
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
Like codegen/1 but raises on errors.
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"]
@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—:staticor: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]
Like collect_imports/2 but raises on errors.
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"]
Like imports/2 but raises on errors.
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
Like minify/3 but raises on errors.
Examples
iex> min = OXC.minify!("const x = 1 + 2;", "test.js")
iex> is_binary(min)
true
@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
Like parse/2 but raises on parse errors.
Examples
iex> ast = OXC.parse!("const x = 1", "test.js")
iex> ast.type
:program
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'"
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
@spec postwalk(ast() | [ast()], acc, (map(), acc -> {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'"
@spec rewrite_specifiers(String.t(), String.t(), (String.t() -> {: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'"
@spec rewrite_specifiers!(String.t(), String.t(), (String.t() -> {:rewrite, String.t()} | :keep)) :: String.t()
Like rewrite_specifiers/3 but raises on errors.
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
@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). Whentrue, 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
@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"
@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
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 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"