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:

counter.js — source
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);
counter.js — amo build (browser target)
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.

counter.js — amo build (server target)
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 === '&' ? '&amp;' : '&lt;'));
const _$ea = (s) =>
  s.replace(/[&"<]/g, (c) => (c === '&' ? '&amp;' : c === '"' ? '&quot;' : '&lt;'));
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.