Skip to the contentSkip to the navigation
RootsGitHubMenu
On this page

Getting started

Roots runs on SBCL (a Common Lisp implementation) and uses Node.js to run what it builds (tests, the terminal target, pre-rendering).

Install #

$ brew install sbcl          # or: apt install sbcl
$ git clone https://github.com/marcuspuchalla/roots
$ roots/bin/roots version

Node.js 22 or newer must be on the PATH. roots doctor shows which targets this machine can build natively (Xcode for iOS, the Android SDK for Android).

The toolchain is the script bin/roots of the checkout. The rest of the documentation writes roots; either put it on the PATH, or write the toolchain as one executable:

$ export PATH="$PWD/roots/bin:$PATH"            # the script, compiled on first use
$ roots/bin/roots self binary --out ~/bin/roots # or: one file, starts in milliseconds

(The single binary says what is in it.)

Create and run an app #

An app is a directory of its own, anywhere:

$ roots new my-app && cd my-app   # roots.lisp, src/App.vue, a component
$ roots dev                       # http://localhost:5173/, live reload
$ roots run                       # the same app in the terminal
$ roots build --target pwa        # → dist/pwa/

roots dev rebuilds on every save and keeps the page's state when only styles changed. The output of roots build is a directory of static files: it needs no server features.

The project #

This is what roots new writes:

my-app/
  roots.lisp                  the project file: name, entry, theme, targets (data, never evaluated)
  src/App.vue                 the entry component
  src/components/Counter.vue  a component that App.vue uses
  src/hint.ts                 a module, and its variant for one target:
  src/hint.term.ts            the terminal build takes this file instead
  README.md  .gitignore

More components and modules (.vue, .ts, .js, .lisp) go under src/. A directory named by :public in roots.lisp is copied as it is into the DOM targets (the scaffold has none).

Every key of roots.lisp is listed in the reference; Building apps explains them.

A component #

A component is a single file: a script, a template, optional styles. This one runs here:

A counterstatic previewdemos/counter.vue
0twice that is 0
counter.vue
<script setup lang="ts">
import { computed, ref } from "roots/runtime"

const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>

<template>
  <Box class="counter flex-row items-center gap-3">
    <Button label="−" aria-label="One less" @click="count--" />
    <Text class="counter-value">{{ count }}</Text>
    <Button label="+" aria-label="One more" @click="count++" />
    <Text class="counter-note">twice that is {{ doubled }}</Text>
  </Box>
</template>

<style>
.counter { flex-wrap: wrap; }
.counter-value { min-width: 2ch; text-align: center; font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.counter-note { color: var(--roots-muted); }
</style>
  • ref(0) is a reactive value; the template reads it as count, the script as count.value.
  • computed derives a value and updates when what it read changes.
  • Box, Text and Button are host primitives: they render as HTML elements in a browser and as cells in a terminal. Plain HTML tags work too, on DOM targets.

The same component with a Lisp script (the template is the same language):

A counter, in Lispstatic previewdemos/counter-lisp.vue
0twice that is 0
counter-lisp.vue
<script setup lang="lisp">
(import "roots/runtime" ref computed)

(defvar count (ref 0))
(defvar doubled (computed (lambda () (* 2 (@ count value)))))
(defun more () (incf (@ count value)))
(defun less () (decf (@ count value)))
</script>

<template>
  <Box class="counter-lisp flex-row items-center gap-3">
    <Button label="−" aria-label="One less" @click="less" />
    <Text class="counter-lisp-value">{{ count }}</Text>
    <Button label="+" aria-label="One more" @click="more" />
    <Text class="counter-lisp-note">twice that is {{ doubled }}</Text>
  </Box>
</template>

<style>
.counter-lisp { flex-wrap: wrap; }
.counter-lisp-value { min-width: 2ch; text-align: center; font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.counter-lisp-note { color: var(--roots-muted); }
</style>

Where next #