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 versionNode.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 .gitignoreMore 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:
<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 ascount, the script ascount.value.computedderives a value and updates when what it read changes.Box,TextandButtonare 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):
<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 #
- Components by example: state, forms, lists.
- Routing by example: pages, links, guards.
- Templates and single-file components: the whole template language.
- The six targets: what each target produces and needs.