ArcMoon 1.1.0

Start

Language

Reference

Nothing matches “”.

↑↓ move Enter open Esc close

Language

Runtime code

runtime ${ }$: JavaScript in the browser, events, refs, URL imports, await, and optional reactivity.

What it is

runtime ${ }$ is plain JavaScript that runs in the visitor's browser, after the page loads. Click the button:

index.arcm
[button = onclick: runtime ${ (e) => (e.currentTarget.textContent = "Thanks!") }$]
  Click me
[end]
Result
  • ${ }$ runs once, while building, and writes HTML. runtime ${ }$ runs in the browser and can change the page.
  • ArcMoon bundles the runtime code with esbuild and adds it to the page. Pages without it ship no JavaScript.

How it works

It runs in the browser, after the page loads

The code goes into the page's script and runs when the page opens. It only exists in pages ArcMoon builds as HTML.

runtime ${ console.log("Opened at", new Date().toLocaleTimeString()); }$

Each use of a component runs its own copy

Every use gets its own refs, signals and exported values:

Counter.arcm
${ const { start = 0 } = ArcMoon.props(); export const initial = start; }$
runtime ${
  import { signal } from "arcmoon/reactive";
  const count = signal(initial);
}$
[button = onclick: runtime ${ () => count(count() + 1) }$]runtime ${ count() }$[end]
index.arcm
[import = Counter: "./Counter.arcm" !]
[Counter = start: 5 !]    # counts from 5
[Counter = start: 10 !]   # counts from 10, on its own

Rules

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

1. Build time and the browser are two worlds

${ }$ and runtime ${ }$ can't see each other's variables or imports, in either direction:

# ✗ the browser can't see a build-time variable
${ const name = "Adam"; }$
runtime ${ console.log(name); }$

# ✗ the build can't see a runtime variable
runtime ${ const size = 3; }$
[p]${ size }$[end]

2. Only exported values cross over

Export a build-time value to use it in the browser. It is copied once, while building, and only goes that way:

# ✓ right
${ export const name = "Adam"; }$
runtime ${ console.log(name); }$

3. Where you write it decides its kind

At the top level it's a code block: any statements. Inside markup or a prop it's a live value: one expression.

# code block: statements, imports, functions
runtime ${
  const greet = (who) => `Hello, ${who}`;
}$

# live values: one expression each
[p]runtime ${ greet("Adam") }$[end]
[button = onclick: runtime ${ () => alert(greet("you")) }$]Greet[end]

4. One file, one scope

Live values can use anything from the file's runtime blocks. Another file's variables are not visible, not even the page's inside a component used on it:

# page.arcm
[import = Card: "./Card.arcm" !]
runtime ${ const theme = "dark"; }$
[p]runtime ${ theme }$[end]   # ✓ same file
[Card!]

# Card.arcm
[p]runtime ${ theme }$[end]   # ✗ theme belongs to page.arcm

ArcMoon doesn't always catch this while building; in the browser it throws a ReferenceError. Pass values to a component as props instead.

5. A live value only updates if it reads a signal

# ✗ count changes, but the page keeps showing 0
runtime ${ let count = 0; }$
[button = onclick: runtime ${ () => count++ }$]runtime ${ count }$[end]

# ✓ updates on every click
runtime ${
  import { signal } from "arcmoon/reactive";
  const count = signal(0);
}$
[button = onclick: runtime ${ () => count(count() + 1) }$]runtime ${ count() }$[end]

6. Events take a function, not a call

# ✗ save() runs once, when the page loads
[button = onclick: runtime ${ save() }$]Save[end]

# ✓ save() runs on every click
[button = onclick: runtime ${ () => save() }$]Save[end]

7. npm packages must be listed in bundle or packages

bundle bundles a package from node_modules; packages loads it from a URL. Local files, URL imports and arcmoon/reactive need neither. node: modules never work in the browser.

arcmoon.config.js
export default {
  bundle: ["canvas-confetti"],
  packages: { dayjs: "https://esm.sh/dayjs@1" }
};
runtime ${
  import confetti from "canvas-confetti";         // ✓ listed in bundle
  import dayjs from "dayjs";                      // ✓ listed in packages: loaded from its URL
  import { marked } from "https://esm.sh/marked@14"; // ✓ a URL: the browser loads it
  import { add } from "./add.js";                 // ✓ local file
  import fs from "node:fs";                       // ✗ not in the browser
}$

8. export and ArcMoon.props() are build-time only

Read props in ${ }$ and export what runtime code needs:

# ✗ wrong
runtime ${
  export const x = 1;
  const { start } = ArcMoon.props();
}$

# ✓ right
${
  const { start = 0 } = ArcMoon.props();
  export const initial = start;
}$
runtime ${ console.log(initial); }$

Both stop the build at the spot:

✗ export is not allowed in runtime code
✗ ArcMoon.props() only exists at build time; read props in ${ }$ and export what runtime code needs

9. await at the top of a block, not in a live value

A code block can await (ArcMoon 1.1). A live value can't: live values update at once, so wait in a block and put the result in a signal. See Waiting for data.

# ✓ right
runtime ${
  import { signal } from "arcmoon/reactive";
  const user = signal(await (await fetch("/me.json")).json());
}$
[p]runtime ${ user().name }$[end]

# ✗ wrong
[p]runtime ${ await load() }$[end]

Code blocks and live values

Where you write it decides what it is:

  • At the top level of the file: a code block. Imports, variables, functions, any statements.
  • Inside markup or a prop: a live value. One expression, no ;.
runtime ${
  const hello = "Hi from the browser";
}$
[p]runtime ${ hello }$[end]
[p]Window width: runtime ${ innerWidth }$px[end]

All runtime code in one file shares its variables. A live value is filled in when the page loads; to make it update later, see Reactivity.

Events

Props starting with on add an event listener. They take a function, which gets the event:

  • onclick: runtime ${ () => save() }$ calls save() on every click.
  • onclick: runtime ${ save() }$ calls it once, when the page loads, and never on click.
  • oninput: runtime ${ (e) => … }$: e.currentTarget is the element, e.target.value what was typed.

Refs

To work with an element directly (focus it, measure it, give it to a library), mark it with arcm-ref and get it with ArcMoon.ref():

index.arcm
[input = arcm-ref: "box", placeholder: "Type here" !]
[button = arcm-ref: "go"]Focus the box[end]
runtime ${
  const box = ArcMoon.ref(ArcMoon.defineRef("box"));
  ArcMoon.ref(ArcMoon.defineRef("go")).onclick = () => box.focus();
}$
Result

For many elements, use arcm-shared-ref and ArcMoon.refs(), which gives a list:

[li = arcm-shared-ref: "item"]Tea[end]
[li = arcm-shared-ref: "item"]Cake[end]
runtime ${
  const items = ArcMoon.refs(ArcMoon.defineRef("item"));
  items.forEach((li) => (li.onclick = () => li.remove()));
}$

Don't invent ids: a component used twice would repeat them. Refs stay unique for every use.

Values from build time

Runtime code can't see ${ }$ variables. To send one to the browser, export it:

index.arcm
${ export const tips = ["Drink water", "Take a walk", "Call a friend"]; }$
[button = arcm-ref: "next"]Show a tip[end]
[p = arcm-ref: "tip"]Click the button.[end]
runtime ${
  const tip = ArcMoon.ref(ArcMoon.defineRef("tip"));
  let n = 0;
  ArcMoon.ref(ArcMoon.defineRef("next")).onclick = () => (tip.textContent = tips[n++ % tips.length]);
}$
Result
  • The value is copied once, at build time, and goes one way: build time → browser.
  • Text, numbers, booleans, arrays, plain objects, Date, Map and Set can be exported. Functions can't.
  • If a value never changes, skip runtime code: ${ value }$ writes it into the HTML with no script.

npm packages

List the browser packages you import in bundle. Local files don't need it:

arcmoon.config.js
export default {
  bundle: ["canvas-confetti"]
};
index.arcm
runtime ${ import confetti from "canvas-confetti"; }$
[button = onclick: runtime ${ () => confetti() }$]Party[end]

Or don't install it: import it from a URL, and the visitor's browser loads it (ArcMoon 1.1). With packages in the config, a short name stands for the URL, subpaths included (dayjs/plugin/utc):

index.arcm
runtime ${ import confetti from "https://esm.sh/canvas-confetti@1"; }$
[button = onclick: runtime ${ () => confetti() }$]Party[end]
arcmoon.config.js
export default {
  packages: { dayjs: "https://esm.sh/dayjs@1" }
};

Waiting for data

A code block can await at its top level, for data or a module (ArcMoon 1.1):

runtime ${
  import { signal } from "arcmoon/reactive";
  const books = signal(await (await fetch("/books.json")).json());
}$
[ul]
  [for-each = runtime ${ books() }$, as: "book", key: "id"]
    [li]runtime ${ book.title }$[end]
  [end]
[end]
  • The file's live values and events are attached after its code has run, so after the waiting. Until then the page shows its HTML.
  • To keep a page usable while data loads, start the signal empty and set it when the data comes, instead of waiting at the top.
  • An error after an await is reported like any other runtime error.

Reactivity (optional)

Everything above works without importing it. When a value on the page should update by itself, use arcmoon/reactive: a small signals library built on alien-signals. Click the button:

index.arcm
runtime ${
  import { signal } from "arcmoon/reactive";
  const count = signal(0);
}$
[button = onclick: runtime ${ () => count(count() + 1) }$]
  Clicked runtime ${ count() }$ times
[end]
Result
  • signal(0) holds a value. count() reads it; count(5) sets it.
  • A live value that reads a signal updates whenever the signal changes: text, attributes, and css. / -- style props.

computed makes a value from other signals, and effect runs code each time the signals it reads change. Type a name:

index.arcm
runtime ${
  import { signal, computed, effect } from "arcmoon/reactive";
  const name = signal("");
  const long = computed(() => name().length > 5);
  effect(() => (document.title = name() || "Hello"));
}$
[input = placeholder: "Your name", oninput: runtime ${ (e) => name(e.target.value) }$ !]
[p = css.color: runtime ${ long() ? "crimson" : "inherit" }$]
  Hello, runtime ${ name() || "stranger" }$!
[end]
Result

A plain variable doesn't update the page. The code changes it, but the page keeps showing the first value:

# ✗ the number on the page never changes
runtime ${ let count = 0; }$
[button = onclick: runtime ${ () => count++ }$]runtime ${ count }$[end]

# ✓ updates on every click
runtime ${
  import { signal } from "arcmoon/reactive";
  const count = signal(0);
}$
[button = onclick: runtime ${ () => count(count() + 1) }$]runtime ${ count() }$[end]

More in Reactivity: computed, effect, lists that change, and sharing signals between components.

Tips

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

Reach elements with refs, not ids you invent

An id repeats when a component is used twice. Refs stay unique for every use:

# ✗ breaks when this is used twice
[input = id: "search" !]
runtime ${ document.getElementById("search").focus(); }$

# ✓ right
[input = arcm-ref: "search" !]
runtime ${ ArcMoon.ref(ArcMoon.defineRef("search")).focus(); }$

Values that never change don't need runtime code

# ✗ ships a script just to show a fixed value
${ export const version = "1.0.0"; }$
[p]runtime ${ version }$[end]

# ✓ written into the HTML, no script
${ const version = "1.0.0"; }$
[p]${ version }$[end]

Mistakes

Each message below stops the build (✗) or warns (⚠). Here is what causes it and how to fix it.

A build-time value that isn't exported

✗ runtime code uses "siteName", which is not exported from ${ }$. Add "export" to its declaration, or write export { siteName }, to send it to visitors' browsers.

${ }$ runs while building and runtime ${ }$ runs in the browser, so they can't see each other's variables. Only exported values are copied into the page:

# ✗ wrong
${ const siteName = "My site"; }$
runtime ${ console.log(siteName); }$

# ✓ right
${ export const siteName = "My site"; }$
runtime ${ console.log(siteName); }$

Exported values are visible to every visitor. Never export secrets like API keys.

A [for-each] name in runtime code

✗ runtime code uses "item", a [for-each] name that only exists at build time. To use it in the browser, write it into the markup (for example data-item: ${ item }$) and read it there.

The loop runs while building, so item is gone by the time the page is in the browser. Write the value into the element, then read it from the event:

# ✗ wrong
[for-each = ${ ["Tea", "Cake"] }$, as: "item"]
  [button = onclick: runtime ${ () => alert(item) }$]${ item }$[end]
[end]

# ✓ right
[for-each = ${ ["Tea", "Cake"] }$, as: "item"]
  [button = data-item: ${ item }$, onclick: runtime ${ (e) => alert(e.currentTarget.dataset.item) }$]${ item }$[end]
[end]

A ref name that doesn't match

✗ defineRef("my-bnt") matches no arcm-ref in page.arcm (did you mean "my-btn"?)

defineRef() takes the name of an arcm-ref or arcm-shared-ref in the same file (or in a component used in it). ArcMoon checks the name while building, so a typo never reaches the browser. Fix the spelling.

An npm package not listed in bundle

✗ runtime import "canvas-confetti" is not listed in bundle (arcmoon.config.js)

Packages in runtime code are sent to every visitor, so ArcMoon only bundles the ones you list. Install the package, then add it to bundle:

npm install canvas-confetti
arcmoon.config.js
export default {
  bundle: ["canvas-confetti"]
};

A Node.js module in the browser

✗ runtime import "node:fs" is a Node.js module, which doesn't exist in the browser

Browsers have no node:fs, node:path and so on. Do that work in ${ }$ while building, and export the result:

# ✗ wrong
runtime ${
  import { readFileSync } from "node:fs";
  const notes = readFileSync("notes.txt", "utf8");
}$

# ✓ right
${
  import { readFileSync } from "node:fs";
  export const notes = readFileSync("notes.txt", "utf8");
}$
runtime ${ console.log(notes); }$

A name runtime code doesn't have

⚠ page.arcm:2:4  "cout" is not defined in this file's runtime code (did you mean "count"?)

A warning: the build goes on, but in the browser cout() throws a ReferenceError and that code stops. Usually a typo, so fix the name. Browser globals like window or document never warn.

Statements inside markup

✗ runtime ${ }$ inside [div] must be one expression (a live value).
  To run statements, move this runtime ${ }$ to the top level of the file.

Inside markup, runtime ${ }$ is a live value: one expression. Move statements to a block at the top level:

# ✗ wrong
[div]runtime ${ const total = 2 + 3; total }$[end]

# ✓ right
runtime ${ const total = 2 + 3; }$
[div]runtime ${ total }$[end]