ArcMoon 1.1.0

Start

Language

Reference

Nothing matches “”.

↑↓ move Enter open Esc close

Language

Build-time code

${ }$: Node.js that runs while the page is built.

What it is

${ }$ is JavaScript that runs in Node.js while the page is built. Only its result goes into the HTML; the browser never sees the code:

${ const year = 2026; }$
[footer]© ${ year }$ My site[end]
HTML ArcMoon builds
<footer>© 2026 My site</footer>

How it works

It runs in Node.js, while building

The code runs once per build, before any visitor sees the page. Its console.log prints in your terminal, not in the browser:

${ console.log("Building the home page"); }$

It runs again for each use of a component

Every use gets its own props, so the same code can write different HTML:

[Card = title: "First" !]    # Card.arcm runs with title "First"
[Card = title: "Second" !]   # and again with title "Second"

Rules

Break one of these and your code stops the build or doesn't do what you expect.

1. There is no browser

document, window and the page's elements don't exist yet. Use runtime code for those:

# ✗ ReferenceError: document is not defined
[p]${ document.title }$[end]

# ✓ runs in the browser
[p]runtime ${ document.title }$[end]

2. The last expression is the output

For several statements, end with return. It must be the last statement, not inside an if:

# ✗ return is only allowed as the last statement of ${ }$
[p]${ if (ready) { return "Ready"; } }$[end]

# ✓ right
[p]${ ready ? "Ready" : "Not yet" }$[end]

3. Output text, numbers, { raw }, arrays, or nothing

Other objects stop the build. Output a field of the object instead:

${ const user = { name: "Adam" }; }$

# ✗ can't render [object Object]; return text, { raw }, or an array
[p]${ user }$[end]

# ✓ right
[p]${ user.name }$[end]

4. Variables live where you make them

At the top of the file, they work everywhere below. Inside an element, they end when it closes:

${ const site = "My site"; }$
[header]${ site }$[end]          # ✓ top level: works below

[div]${ const inner = 1; }$[end]
[p]${ inner }$[end]              # ✗ ReferenceError: inner is not defined

5. Components use [import], not JavaScript import

# ✗ Unknown file extension ".arcm"
${ import Card from "./Card.arcm"; }$

# ✓ right
[import = Card: "./Card.arcm" !]

6. ArcMoon never installs packages

Install them first, or the import fails:

npm install dayjs
${ import dayjs from "dayjs"; }$
[p]Built ${ dayjs().format("D MMM YYYY") }$[end]

7. Read a component's props at the top of its file

Inside an element, ArcMoon.props() returns that element's props instead:

Card.arcm
${ const { title } = ArcMoon.props(); }$   # ✓ the component's props
[h2]${ title }$[end]
[h2]${ ArcMoon.props().title }$[end]       # ✗ the [h2]'s props
⚠ Card.arcm:3:7  ArcMoon.props() inside [h2] returns the [h2]'s props, which have no "title"; for the component's props, call it at the top of the file

8. Code has a time limit

Each page gets 5 seconds, its components included. Slower code is stopped; raise timeout in arcmoon.config.js if a page needs more:

arcmoon.config.js
export default { timeout: 10000 };

What it outputs

The last expression is the output. Try it: change the values and press Run:

index.arcm
[p]${ 6 * 7 }$[end]
[p]${ "<b>escaped</b>" }$[end]
[p]${ { raw: "<b>written as HTML</b>" } }$[end]
[p]${ ["one", " two", " three"] }$[end]
[p]Nothing here: ${ null }$${ false }$[end]
Result
  • Text and numbers are escaped, so <b> shows as text.
  • { raw: "…" } writes HTML as it is.
  • An array writes each item.
  • null, undefined, true and false write nothing. Other objects stop the build.

For more than one line, end with return:

[p]${
  const items = ["tea", "cake"];
  return items.join(" and ");
}$[end]
HTML ArcMoon builds
<p>tea and cake</p>

Variables

A variable made at the top of the file can be used anywhere below it:

${ const name = "Adam"; }$
[h1]Hi, ${ name }$[end]
[p]${ name.toUpperCase() }$[end]
HTML ArcMoon builds
<h1>Hi, Adam</h1>
<p>ADAM</p>

One made inside an element only lives until that element closes:

[div]${ const inner = 1; }$[end]
[p]${ inner }$[end]

✗ ReferenceError: inner is not defined

Imports and files

Import .js and .json files, node: modules and npm packages. Relative paths start from the .arcm file:

site.json
{ "name": "My site", "author": "Adam" }
format.js
export const money = (n) => `$${n.toFixed(2)}`;
index.arcm
${
  import { name, author } from "./site.json";
  import { money } from "./format.js";
  import { readFileSync } from "node:fs";

  const about = readFileSync(new URL("./about.txt", import.meta.url), "utf8").trim();
}$
[h1]${ name }$[end]
[p]${ about }$[end]
[p]By ${ author }$, from ${ money(5) }$ a month.[end]
HTML ArcMoon builds
<h1>My site</h1>
<p>ArcMoon builds pages from .arcm files.</p>
<p>By Adam, from $5.00 a month.</p>
  • JSON works with named imports: import { name } from "./site.json".
  • To read a file next to the page, use new URL("./file", import.meta.url).
  • Components are imported with [import], not with JavaScript import.
  • ArcMoon never installs packages: run npm install yourself.

Waiting for data

await works at the top of ${ }$, so the page can wait for files or an API:

${
  const res = await fetch("https://api.github.com/repos/Adam-Elmi/ArcMoon");
  const repo = await res.json();
}$
[p]${ repo.stargazers_count }$ stars[end]

The ArcMoon object

  • ArcMoon.version: the ArcMoon version, e.g. 1.0.0.
  • ArcMoon.props(): the props a component was given. See Components.

For everything else, use plain Node.js: fetch, process.env, node:fs.

Tips

Your code works either way, but these make it better.

Read files relative to the page

A plain path is read from the folder you run arcm build in, so it breaks when you build from elsewhere. import.meta.url always starts at the .arcm file:

${ import { readFileSync } from "node:fs"; }$

# ✗ depends on where you run arcm build
[p]${ readFileSync("note.txt", "utf8") }$[end]

# ✓ always next to this file
[p]${ readFileSync(new URL("./note.txt", import.meta.url), "utf8") }$[end]

Keep secrets at build time

Use API keys in ${ }$ freely: the code never reaches the browser. But anything you output or export is in the page for every visitor:

${
  const key = process.env.API_KEY;
  const res = await fetch(`https://api.example.com/stats?key=${key}`);
  const stats = await res.json();
}$
[p]${ stats.visits }$ visits[end]   # ✓ only the result is in the page

Use { raw } only for HTML you trust

{ raw } writes HTML as it is, scripts included. For text from users or other sites, output it plainly so it's escaped.

Prefer build-time code for values that never change

${ }$ writes the value into the HTML: no script, and it shows before any JavaScript loads. Use runtime code only for what changes in the browser.

Errors and limits

An error stops the build and points to the spot in the .arcm file:

✗ ReferenceError: undefinedThing is not defined
  page.arcm:1:7

Each page's code gets 5 seconds; change it with timeout in arcmoon.config.js. Slower code is stopped:

✗ page.arcm  ${ }$ code took longer than 5000 ms and was stopped