COMPILER
How the compiler works
The compiler is an optimizer, not a requirement. Your source already runs in the browser; amo build removes the work the runtime would otherwise do on every page load: parsing templates, locating holes, wiring bindings. The output is the vanilla DOM code you would have written — the project's identity benchmark keeps it within 10% of a hand-written reference, in CI, forever.
parse → IR → codegen
Every module that imports @amojs.dev/core is read with acorn. Each html`` is parsed once at build time into an intermediate form that describes intent — holes, bindings, events — and never names a DOM API. Codegen turns that into a hoisted template plus direct node walks. Because the IR knows nothing about the DOM, the server target is a second codegen over the same IR, not a second parser.
What it emits
The counter from the landing page, with a computed added. On the left, what you write; below it, the exact output of compileModule() as generated while this page was built:
import { signal, computed, html, mount } from '@amojs.dev/core';
const count = signal(0);
const double = computed(() => count.value * 2);
const Counter = () => html`<div class="card">
<p>count = ${count} · double = ${double}</p>
<button onclick=${() => count.value++}>+1</button>
</div>`;
mount(Counter, document.body);
import { signal, computed, mount } from '@amojs.dev/core/runtime';
import { tpl as _$t, bindChild as _$child, bindEvent as _$event } from "@amojs.dev/core/compiled";
const _t0 = _$t("<div class=\"card\">\n <p>count = <!----> · double = <!----></p>\n <button>+1</button>\n</div>", [[0,1,1],[0,1,3]]);
const count = signal(0);
const double = computed(() => count.value * 2);
const Counter = () => (() => {
const _r = _t0().firstChild;
const _n0 = _r.childNodes[1].childNodes[1];
const _n1 = _r.childNodes[1].childNodes[3];
const _n2 = _r.childNodes[3];
_$child(_n0, count);
_$child(_n1, double);
_$event(_n2, "click", () => count.value++);
return _r;
})();
mount(Counter, document.body);
Read it top to bottom: the template is a string parsed once (_$t), each instance is a clone, every hole is a numeric path resolved up front, and each binding is one call — _$child, _$attr, _$event. The import moved from @amojs.dev/core to @amojs.dev/core/runtime, the entry without the parser; html itself is gone.
The same module, for the server
amo build ssg and ssr compile the same source with the server target: string concatenation with escaping, evaluated once. No comment markers — those exist for hydration, which AmoJS never does.
import { signal, computed, mount } from '@amojs.dev/core/runtime';
import { isSignal as _$is } from "@amojs.dev/core/runtime";
/* amo ssr helpers — one escape function per position, evaluate once */
const _$et = (s) => s.replace(/[&<]/g, (c) => (c === '&' ? '&' : '<'));
const _$ea = (s) =>
s.replace(/[&"<]/g, (c) => (c === '&' ? '&' : c === '"' ? '"' : '<'));
const _$txt = (v) => (v == null || v === false ? '' : String(v));
const _$u = (v) => (_$is(v) ? v.value : typeof v === 'function' ? v() : v);
const _$h = (h) => ({ __amoHtml: h, toString: () => h });
const _$c = (v) => {
v = _$u(v);
if (v && typeof v.__amoHtml === 'string') return v.__amoHtml;
if (Array.isArray(v))
return v
.map((n) => {
if (!n || typeof n.__amoHtml !== 'string')
throw new Error('amo: array holes take non-fragment nodes only');
return n.__amoHtml;
})
.join('');
return _$et(_$txt(v));
};
const _$a = (n, v) => {
v = _$u(v);
return v == null || v === false
? ''
: ' ' + n + '="' + _$ea(v === true ? '' : String(v)) + '"';
};
const _$b = (n, v) => (_$u(v) ? ' ' + n + '=""' : '');
const _$v = (v) => ' value="' + _$ea(_$txt(_$u(v))) + '"';
const _$ta = (v) => _$et(_$txt(_$u(v)));
const _$e = (n, v) => {
if (typeof v !== 'function')
throw new Error(
'amo: on' + n + ' needs a function, not a ' + (_$is(v) ? 'signal' : typeof v),
);
return '';
};
const count = signal(0);
const double = computed(() => count.value * 2);
const Counter = () => _$h("<div class=\"card\">\n <p>count = " + _$c(count) + " · double = " + _$c(double) + "</p>\n <button" + _$e("click", () => count.value++) + ">+1</button>\n</div>");
mount(Counter, document.body);
Deliberately dumb
The compiler rewrites html`` templates and @amojs.dev/core specifiers, and touches nothing else. It does not reason about module boundaries, lazy loading, closure serialization or dead code — the places where clever compilers break third-party code and produce failures nobody can debug. A local, mechanical source-to-source rewrite can sit next to any other tool in your pipeline: Vite (@amojs.dev/vite), esbuild, a plain copy.
Identical by test
Every golden fixture in the repository runs in both modes and asserts identical behaviour; the uncompiled runtime is the semantic source of truth, and the compiler is only allowed to be faster and smaller. A third mode checks the server string against the client DOM structurally. The measured ratio of compiled bytes to hand-written vanilla is 1.090 against a budget of 1.10.