Lilac
A lightweight frontend framework for Ruby developers who are at home with modern HTML and CSS, and would rather reach for Ruby than JavaScript when adding behavior.
Templates stay as valid HTML5 with data-* directives — no inline
expressions, no embedded code. All logic lives in Ruby; reactivity
is driven by Signals and Effects; everything runs in the browser as
mruby compiled to WebAssembly via mruby-wasm-runtime.
mruby-wasm-runtime (MWR) is a separate repo — the "wasm SDK" that
owns the pinned mruby clone, the wasi-sdk toolchain, and the
mruby-wasm-js JS↔mruby bridge. Lilac builds on top of it; rake
fetches it for you (see "Setup").
Setup
git clone https://github.com/takahashim/lilac
cd lilac
rake # → build/lilac-full.wasmThat's it. On first run rake fetches mruby-wasm-runtime into
vendor/ at the pinned version (see MWR_VERSION in the Rakefile),
bootstraps its wasi-sdk + mruby clone, then builds the bundle. Other
targets mirror the names you'll see in rake -T (rake lilac-compiled,
rake test, …).
Hacking on MWR alongside Lilac? Keep a mruby-wasm-runtime clone
next to lilac/ and the build uses it instead of the vendored copy
(direnv allow, or export MRUBY_WASM_RUNTIME_PATH=/path/to/clone).
Quick start: open and run
Lilac runs in the browser with no build step. Author a single HTML
file with data-* directives and an inline <script type="text/ruby">:
<!DOCTYPE html>
<html>
<body>
<div data-component="Counter">
<button data-on-click="decrement">-</button>
<span data-text="@count">0</span>
<button data-on-click="increment">+</button>
</div>
<script type="text/ruby" id="ruby-source">
class Counter < Lilac::Component
def setup
@count = signal(0)
end
def increment(_ev) = @count.update { |n| n + 1 }
def decrement(_ev) = @count.update { |n| n - 1 }
end
Lilac.register "Counter", Counter
Lilac.start
</script>
<script type="module">
import { createVM } from "../mruby-wasm-runtime/mrbgem/mruby-wasm-js/js/index.js";
const vm = await createVM({ wasm: "./build/lilac-full.wasm" });
vm.evalScript("#ruby-source");
</script>
</body>
</html>The runtime walks the DOM at mount time, parses each data-*
directive, and wires the equivalent bind / bind_input /
bind_list calls automatically. See examples/runtime-only/ for fuller demos
including data-each lists, forms, and shared theme state.
To run the examples locally:
rake serve # builds the wasm bundle, copies mrbgem, starts wsv
# then visit http://127.0.0.1:8000/examples/runtime-only/Build variants
| Variant | Compiler | Mrbgems | Use case |
|---|---|---|---|
lilac-full |
✅ | core + directives + async + router + form + extras + regexp-compat | default, no-build, runtime scanner canonical |
lilac-compiled |
❌ (apps must ship mrbc-compiled IREP via lilac build) |
core + directives + form + regexp-compat | production size optimization (codegen canonical, scanner as runtime fallthrough for package directives) |
Add -release to any target for the optimised (-Os --strip-debug)
variant.
Distribution
Lilac is distributed across two channels (ADR-25 / ADR-28):
| What | Channel | URL / package |
|---|---|---|
lilac-full.wasm + boot helper (Mode 1 = CDN-only browser use) |
GitHub Pages |
https://takahashim.github.io/lilac/v$VERSION/ (after first release tag)
|
lilac-compiled.wasm + bridge files (consumed by lilac-cli) |
rubygems |
gem "lilac-wasm-bin" (release pending wasmtime-rb v45)
|
lilac-cli CLI gem |
rubygems |
gem "lilac-cli" (release pending wasmtime-rb v45)
|
| Package gems (extras / router / async / form) | rubygems |
gem "lilac-extras" etc. (release pending)
|
CLI (optional)
cd cli
bundle install
bundle exec exe/lilac --helpThe CLI is an optional optimization layer for larger projects.
The runtime is the canonical interpreter of data-* directives —
you can ship a Lilac app as plain HTML (see Quick start above).
What the CLI adds on top:
- Static lint — undeclared signals, unused methods, banned attributes, grammar errors caught at build time with source positions (instead of at component mount time).
-
.lilsingle-file components + project structure (components/+pages/) with<div data-use="..."></div>placeholder composition. -
Pre-compiled bindings — directive interpretation moves from
mount time to build time; the generated
Lilac::Bindings::<Class>module'sbind_template_hooktakes precedence over the runtime scanner, so there's no double-binding when both paths coexist. -
dev server with live reload,
scaffold,doctor.
See cli/README.md for usage details.
Design decisions
Architecture-level rationales (= "why does it work this way?") live as
ADRs under docs/adr/. The
index lists each decision; per-ADR files
record the problem, the trade-offs considered, and the resulting
implementation. Speculative / unconfirmed proposals are tracked
separately in docs/lilac-proposals.md.
Tests
rake test # wasm_spec under wasmtime-rb + Dommy (Ruby-only, fast,
# no Node install needed). Default for the inner dev loop.
rake test-node # Same scenarios under Node + happy-dom (V8 cross-check).
# Used in CI; rarely needed in local development.
rake test-cli # Ruby CLI gem tests — fast, no wasm rebuild
rake test-all # CLI + Ruby wasm_spec + Node wasm_spec. Pre-release sweep.
# Equivalent low-level invocation for the CLI tests:
cd cli && bundle exec rake testLicense
MIT