Translates Elixir expressions to JavaScript.
This module handles the core transpilation of Elixir AST to JavaScript code, enabling optimistic client-side evaluation of reactive expressions.
Supported Constructs
Literals
- Strings, numbers, booleans, nil, atoms
- Lists and maps
Operators
- Comparison:
==,!=,>,<,>=,<= - Logical:
&&,||,and,or,not,! - Arithmetic:
+,-,*,/ - String:
<>(concatenation) - List:
++(concatenation),in(membership) - Pipe:
|>
Control Flow
if cond, do: x, else: y→ ternary operator
State References
@variable→state.variable@nested.field→state.nested.field
String Functions
String.length/1,String.trim/1String.to_integer/1,String.to_float/1String.contains?/2,String.starts_with?/2,String.ends_with?/2String.replace/3,String.slice/3,String.match?/2
Enum Functions
Enum.member?/2,Enum.count/1,Enum.join/1,2Enum.map/2,Enum.filter/2,Enum.reject/2
Map Functions
Map.get/2,3
Utility Functions
Usage
iex> Lavash.Transpiler.to_js("@count + 1")
"(state.count + 1)"
iex> Lavash.Transpiler.to_js("if @active, do: \"on\", else: \"off\"")
"(state.active ? \"on\" : \"off\")"Validation
Use validate/1 to check if an expression can be fully transpiled:
iex> Lavash.Transpiler.validate("@count + 1")
:ok
iex> Lavash.Transpiler.validate("Ash.read!(Product)")
{:error, "Ash.read!"}
Summary
Functions
Translates an Elixir AST to JavaScript.
Emit a JS object property access for an Elixir field name.
Emit a JS object literal key for an Elixir field name.
Translates an Elixir expression string to JavaScript.
Checks if an AST can be transpiled to JavaScript.
Transpiles a run function body to JavaScript statements.
Validates that an Elixir expression can be transpiled to JavaScript.
Validates an AST for transpilability.
Functions
Translates an Elixir AST to JavaScript.
This is the lower-level function that works directly with AST.
Use to_js/1 for string input.
Emit a JS object property access for an Elixir field name.
Elixir allows ? and ! in atom names (idiomatic for boolean
predicates and bang-mutating helpers). JS identifiers don't, so
state.is_admin? is a syntax error. Field names that aren't valid
JS identifiers get rewritten to bracket access with a string key:
state["is_admin?"]. Safe names stay as dot access.
Used everywhere a state field becomes a JS property reference — the
rx transpiler's @field handling, optimistic action set deltas,
and the calc/derive emitter.
Emit a JS object literal key for an Elixir field name.
Same rule as js_field_access/2: safe names emit as bare keys
({is_admin: true}); names with ?/!/leading-digit/etc. wrap
as quoted string keys ({"is_admin?": true}).
Translates an Elixir expression string to JavaScript.
Returns the JavaScript code as a string. Untranspilable expressions
are converted to undefined with a comment indicating the issue.
Examples
iex> Lavash.Transpiler.to_js("@count")
"state.count"
iex> Lavash.Transpiler.to_js("length(@items)")
"(state.items.length)"
iex> Lavash.Transpiler.to_js("if @a, do: 1, else: 2")
"(state.a ? 1 : 2)"
Checks if an AST can be transpiled to JavaScript.
Returns true if transpilable, false otherwise.
Use validate/1 or validate_ast/1 to get the specific error.
Transpiles a run function body to JavaScript statements.
This handles the run [:reads], fn assigns -> ... end pattern, where:
assigns.fieldaccesses →state.fieldx = exprbindings →const x = expr;assign(assigns, :field, value)→ adds to return object- Piped assigns with multiple
assign/3calls → combined return object
Returns a tuple of {statements, return_expr} where statements is a list
of JS statements (const declarations) and return_expr is the return object.
Examples
# Simple case
iex> body = quote do
...> discount = assigns.subtotal * assigns.discount_rate
...> assigns |> assign(:discount_amount, discount) |> assign(:total, assigns.subtotal - discount)
...> end
iex> Lavash.Rx.Transpiler.transpile_run_body(body)
{["const discount = (state.subtotal * state.discount_rate);"],
"{discount_amount: discount, total: (state.subtotal - discount)}"}
Validates that an Elixir expression can be transpiled to JavaScript.
Returns :ok if the expression is fully transpilable, or
{:error, description} if it contains unsupported constructs.
Examples
iex> Lavash.Rx.Transpiler.validate("length(@tags)")
:ok
iex> Lavash.Rx.Transpiler.validate("Ash.read!(Product)")
{:error, "Ash.read!"}
Validates an AST for transpilability.
Returns :ok if transpilable, {:error, reason} otherwise.