Reference
Browser build
Compile ArcMoon in the browser: render(), compile(), files, baseUrl and packages.
What it is
ArcMoon also runs in the browser: build a page from ArcMoon code and show it, with no server. It is made for live editors, previews and tools. The Playground on this site uses it. For real sites, build with the CLI in Node.js.
No install needed, straight from a CDN:
<!doctype html>
<div id="app"></div>
<script type="module">
import ArcMoon from "https://cdn.jsdelivr.net/npm/arcmoon@1/+esm";
const am = new ArcMoon({ src: "[p]From the CDN: ${ 6 * 7 }$[end]" });
document.getElementById("app").append(await am.render());
</script>Getting it
- From npm:
npm install arcmoon, thenimport ArcMoon from "arcmoon". Bundlers pick the browser build (dist/browser.jsanddist/worker.js) by themselves. - From a CDN:
import ArcMoon from "https://cdn.jsdelivr.net/npm/arcmoon@1/+esm".
The compile runs in a Web Worker. Browsers only start workers from the page's own site, so from a CDN, ArcMoon starts it through a blob: file. If your page has a Content Security Policy, allow worker-src blob:.
render()
Builds the page as real elements, ready to put in the page. Runtime code works:
import ArcMoon from "arcmoon";
const am = new ArcMoon({
src: `
runtime \${
import { signal } from "arcmoon/reactive";
const count = signal(0);
}$
[button = onclick: runtime \${ () => count(count() + 1) }$]Clicked runtime \${ count() }$[end]
`
});
const app = document.getElementById("app");
app.append(await am.render()); // insert it right away: runtime code starts on the next task
// clicking the button counts up- It returns a document fragment. Insert it right after
await am.render(): runtime code starts on the next task. - Refs point straight at the built elements, and the
data-arcm-refattributes are removed. { raw }HTML is turned into real elements too.
compile()
Returns the page as an HTML string, like in Node.js:
const html = await new ArcMoon({ src: "[p]Hi ${ 20 + 1 }$[end]" }).compile();
// "<p>Hi 21</p>"If the page has runtime code, its script loads its code from blob: URLs that only exist while this page is open. So the HTML runs in this page, in a frame or a new tab of it, but not as a saved file:
import ArcMoon from "arcmoon";
const html = await new ArcMoon({ src: "…" }).compile();
// show it in a frame of this page: its runtime code works there
const frame = document.querySelector("iframe");
frame.srcdoc = html;To make HTML files, build in Node.js.
Files in memory
There is no disk in the browser. Give the files as a map of names to code, and they can import each other:
import ArcMoon from "arcmoon";
const html = await new ArcMoon({
filename: "index.arcm",
files: {
"index.arcm": '[import = Card: "./Card.arcm" !][Card = title: "Virtual"]Body[end]',
"Card.arcm": '${ const { title } = ArcMoon.props(); }$[div][h3]${ title }$[end][slot!][end]'
}
}).compile();
// <div><h3>Virtual</h3>Body</div>filenamepicks which file is the page. Without it, usesrcfor the page code.- Paths are relative, as on disk.
importAliasesworks too. .jsand.jsonfiles infilescan be imported by build-time and runtime code.
Files from a server
With baseUrl, files not in files are fetched from that address: components, JSON, scripts:
import ArcMoon from "arcmoon";
// https://example.com/site/components/Card.arcm and https://example.com/site/data.json
const html = await new ArcMoon({
src: `
[import = Card: "./components/Card.arcm" !]
\${ import { name } from "./data.json"; }$
[Card = title: \${ name }$]From the server[end]
`,
baseUrl: "https://example.com/site/"
}).compile();
// <div class="card"><h3>Fetched site</h3>From the server</div>The server must allow the request (CORS), like any fetch from the page.
npm packages
The browser can't read node_modules. Map each package name to a URL that serves it as a module, like esm.sh:
import ArcMoon from "arcmoon";
const html = await new ArcMoon({
src: '${ import dayjs from "dayjs"; }$[p]${ dayjs("2026-10-15").format("D MMM YYYY") }$[end]',
packages: { dayjs: "https://esm.sh/dayjs@1.11.13" }
}).compile();
// <p>15 Oct 2026</p>The same map serves build-time and runtime code. A package that isn't in it stops the build:
✗ /anonymous.arcm "dayjs" is not in packages; add packages: { "dayjs": "https://esm.sh/dayjs" }Props and env
const html = await new ArcMoon({
src: '${ const { user } = ArcMoon.props(); }$[p]${ process.env.API_URL }$ for ${ user }$[end]',
props: { user: "Adam" },
env: { API_URL: "https://api.example.com" }
}).compile();
// <p>https://api.example.com for Adam</p>propsworks as in Node.js:ArcMoon.props()at the top of the page reads it.envis the wholeprocess.env: the browser has no environment of its own. Each compile gets only theenvpassed to it.
What build-time code can use
Build-time code runs in the browser's worker, not in Node.js, so some things work differently:
- npm packages: from
packagesonly. - Local
.js/.json: fromfilesorbaseUrl. node:path,node:events,node:buffer,node:url: browser stand-ins, also by bare name ("path").node:fs:readFileSync,existsSyncandpromises, reading the files in memory (and fetched ones), not a disk.fetch: the browser's own; CORS applies.- Other Node.js modules: an error.
${ import { readFileSync } from "node:fs"; }$
[pre]${ readFileSync("/notes.txt", "utf8") }$[end]✗ /anonymous.arcm "node:child_process" is not available in the browserThe stand-ins are close to Node.js but not the same: for example path.win32 works like posix, and printing a module object can differ. Code that leans on Node.js details is best built in Node.js. In runtime code, every node: module is an error, as always.
What the worker can't touch
The whole compile (imports, warnings, build-time code) runs in a Web Worker, away from your page. Build-time code there has no document, cookies or storage:
await new ArcMoon({ src: "[p]${ typeof document }$ ${ typeof localStorage }$[end]" }).compile();
// "<p>undefined undefined</p>"- Each compile has a time limit (
timeout, default 5000 ms). A worker that runs over is stopped:${ }$ code took longer than 500 ms and was stopped. - It can still
fetchwhat the browser allows. The protector doesn't run in the browser, so only compile code you trust, or show results in a sandboxed frame. - Runtime code from
render()runs in your page, with its full access.
All options
new ArcMoon({
src: "…", // the page code, or
filename: "index.arcm", // the page, from files or baseUrl
files: { "Card.arcm": "…" }, // files in memory
baseUrl: "https://example.com/site/", // where other files are fetched from
packages: { dayjs: "https://esm.sh/dayjs" }, // npm packages, by URL
props: { user: "Adam" }, // page data for ArcMoon.props()
env: { API_URL: "…" }, // process.env in build-time code
importAliases: { "@": "./src" },
removeComments: true,
timeout: 5000, // ms, then the worker is stopped
onWarning: (w) => {}, // { source, position, message }
worker: myWorker, // for tools: your own worker
toModuleURL: (code) => url // for tools and tests: how code becomes a module URL
});Warnings and errors
The same as in Node.js: warnings go to onWarning as { source, position, message }, and errors throw with name, source and position. See JavaScript API.
try {
await new ArcMoon({ src: "[p = id: AKZB]x[end]" }).compile();
} catch (err) {
err.name; // "ParserError"
err.message; // "/anonymous.arcm:1:10 unquoted value AKZB for prop \"id\"; …"
}Size
browser.js is about 400 kB and worker.js about 390 kB (minified, before compression). Most of it is the HTML reader and writer. Load ArcMoon only on pages that need it, like an editor.