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 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.
$ 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.
$ 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.