COMPILER

The amo CLI

Four commands, one grammar. build, dev and serve each take a mode — csr for the browser, ssg for static pages, ssr for per-request rendering — and eject hands the runtime over. Install it as a dev dependency, or run it with npx @amojs.dev/cli.

Usage

amo --help
amo build [mode] [src] [out]          mode: csr (default) | ssr | ssg
                                      src defaults to src/, out to dist/
amo dev [mode] [src] [out] [--port <n>]
                                      build, watch <src>, rebuild on change,
                                      serve <out> (default port 4700)
amo serve [mode] [dir] [--port <n>]   serve a built <dir> (default dist):
                                      csr | ssg static; ssr renders
                                      <dir>/pages/ per request
amo eject <src> <out> [--runtime <dir>]
amo --help | --version

A bare word that is a mode always reads as the mode; a directory literally named ssr is written ./ssr. Removed forms fail with a pointed message, never a bare usage dump.

terminal
$ amo build              # csr: src/ → dist/, for the browser
$ amo build ssr          # server target + automatic islands pass
$ amo build ssg          # static .html from src/pages/ + islands pass
$ amo build ssg out/     # custom output dir
$ amo build ssr app/ out/   # fully explicit

amo build

Walks the project, compiles every module that imports @amojs.dev/core, and copies everything else through untouched. The output is self-contained: core's prebuilt browser bundle is copied to <out>/_amo/ and every core import is rewritten to reach it relatively — no import map to write, one core url for the whole app.

<out> is emptied before the build when it sits inside the project, so a source file you deleted cannot survive as a stale artifact. <src>/public/ is copied verbatim to the root of <out> — icons, robots.txt, fonts, anything whose url must be exact.

amo dev

One build, then watch, rebuild on change, and serve with pretty urls (/docs/ answers from docs/index.html). A failing rebuild prints its error and keeps serving the last good build. Open tabs update themselves, each change taking the cheapest honest path: a css edit swaps stylesheets in place and island state survives; an island edit re-imports and re-mounts that island (its local state resets); anything else reloads the page, because server-rendered HTML has no client module to swap. The channel is one SSE endpoint and a small client injected into served HTML only — dist/ stays byte-identical to production.

terminal
$ amo dev ssg            # the docs-site loop: src/ → dist/, on :4700
$ amo dev ssr            # server-rendered: every request renders fresh
$ amo dev                # csr projects work too

amo serve

Runs what build made. For csr and ssg it is a static server with pretty urls; for ssr it is the server — static files plus every other url mapped to a compiled page module, rendered per request with { url }. Deliberately dumb: no route params, no middleware, no config, no compression. When you outgrow it, your own server is about ten lines. Port: --port, else $PORT, else 3000.

Islands are a folder, not a flag

With ssr and ssg, if <src>/islands/ exists it is compiled for the browser to <out>/islands/ automatically; no folder means a fully static site, and the pass is skipped out loud. A flag you could forget would ship a site whose every island script 404s; a folder's existence cannot be forgotten. --islands <dir> renames it; --pages <dir> renames pages/.

TypeScript input

Write .ts; the build strips types with node's own type-stripper (node 22.13+). Erasable syntax only, and import the emitted path (./x.js). Type checking stays your toolchain's job.