OPEN SOURCE · MIT

Compiles to the vanilla JavaScript you would have written

AmoJS is a build-time compiler for fine-grained reactive UI. Signals and html`` templates in standard .js files — no custom file format, no virtual DOM. It runs in the browser with no build step; the compiler is an optimizer you add when you ship.

Get started
✓ 1.98 KB all-in ✓ 0 dependencies ✓ ejectable
COUNTER.JS — WHAT YOU WRITE
import { signal, html, mount } from '@amojs.dev/core';

const count = signal(0);

const bump = () => count.value++;

const Counter = () =>
  html`<button onclick=${bump}>${count}</button>`;

mount(Counter, document.body);
↓ AMO BUILD — WHAT SHIPS
import { signal, mount } from '@amojs.dev/core/runtime';
import { tpl as _$t, bindChild as _$child, bindEvent as _$event } from "@amojs.dev/core/compiled";
const _t0 = _$t("<button><!----></button>", [[0,0]]);


const count = signal(0);

const bump = () => count.value++;

const Counter = () =>
  (() => {
  const _r = _t0().firstChild;
  const _n0 = _r.firstChild;
  _$child(_n0, count);
  _$event(_r, "click", bump);
  return _r;
})();

mount(Counter, document.body);
1.98 KB
compiled application, all-in, min+gzip
0
runtime dependencies
12
API names, total
1.090×
compiled vs. handwritten vanilla

HOW IT WORKS

Less framework. More JavaScript.

Write standard .js

Signals for state, tagged templates for markup. No custom file format, no JSX transform, no hook rules. Your app runs in the browser with no build step at all.

Compile when you ship

amo build csr | ssr | ssg replaces the template machinery with precise, pre-computed DOM operations. Each hole becomes one targeted update; the template parser ships nowhere.

Ship direct DOM code

One effect per binding, one write per change. No virtual DOM, no diffing, no tree walks — the same code a careful developer would have written by hand.

amo — zsh
$ npx amo eject src dist
amo eject — 2 compiled, 10 copied, 9 runtime files → dist (runtime: amo-runtime/)
runtime taken from node_modules/@amojs.dev/core/src
no bare "@amojs.dev/core" imports remain — deleting amo changes nothing.

THE EXIT IS PART OF THE DESIGN

amo eject

One command compiles your app a final time, hands the runtime over as readable source files, and rewrites every import to a relative path. What remains is a vanilla JavaScript project you own completely.

Nothing about your architecture changes — because compiled AmoJS already was vanilla JavaScript.

THE WHOLE API

Twelve names. That's the surface.

signal()reactive state
computed()derived values
effect()reactions
isSignal()is this reactive?
flushSync()run pending effects now
tick()await the batch
root()a detached scope
onCleanup()teardown
onMount()after insertion
htmltagged templates
mount()attach to the DOM
each()keyed lists

Write JavaScript. Ship JavaScript.

Runs in the browser today, no build step. Compile when it counts.