Changelog
Roots follows semantic versioning (MAJOR.MINOR.PATCH). Every commit bumps the version; 0.1.0 is the first beta: one app source building for web, PWA, browser extension, iOS, Android and the terminal.
0.4.0: developer and user experience, and a bookstore #
A new example, examples/bookstore ("Roots Books": a SQLite catalogue with search and filters, a cart, checkout, orders and an admin page, for web, PWA and the terminal, with six test suites), was built as a real app and every problem met on the way was logged: 62 entries. This release fixes the ones in the router, the UI kit, the data layer, theming, the terminal and the dev server, each with a regression test, then reviewed by three independent passes (end user and accessibility, API and semantics, tests) whose findings are fixed too. The entries in the template compiler, the build commands and TypeScript support follow in the next release.
Router
- Router: a route's
componentcan be a loader,() => import("./Page.vue"). The page loads on first visit and the build keeps it in a file of its own. - Router: routes can have
redirect(a path, a location or a function),alias,beforeEnterandchildren; a parent renders its child with its own<RouterView/>. - Router: a route definition the router cannot honour (an unknown key,
:id?,:rest+, a pattern param,*inside a path) throws R403 atcreateRouter, naming the route and the key, instead of being ignored. - Router: after a navigation the window scrolls. Back and Forward return to where the reader was, a
#hashgoes to its element, a new page starts at the top.createRouter({ scrollBehavior })decides otherwise. Positions are kept inhistory.state, written while the reader scrolls, and survive a reload. - Router:
createRouter({ focus: "h1" })moves the keyboard focus to the new page's heading. - Router:
to.navigationTypeis"push","replace"or"pop";router.afterRender(hook)runs after the new page has rendered. - Router:
onBeforeRouteLeaveandonBeforeRouteUpdatefor components. - Router:
router.onError(hook)sees every failed navigation; a lazy page that fails to load is R408. - Router: the address bar always names the page on screen: a Back or Forward that a guard cancels, that is redirected to a page which is then refused, or that is overtaken by a refused navigation returns the history to where the reader was; a guard that redirects to the current route, or cancels a typed address, puts the current address back.
- Router: an alias must have every param of its route (R403 otherwise), so
props: truealways has its values. - Router: a function inside a route's
propsobject is passed to the page as it is; it used to be called as a getter. - Router: a setup function without arguments used directly as a route component must be wrapped in
defineComponent, because such a function is now a loader.
UI kit
- A Select, Menu or Popover inside a Dialog or Drawer now opens above it, and the keyboard works in it; a Dialog opened from a Popover covers that popover.
- A loading Button keeps the keyboard focus: it is aria-disabled and aria-busy instead of disabled.
- An invalid Input shows the error border, and focused Inputs, Textareas and Selects show a solid focus outline.
- A screen reader now hears a Select's chosen value: its name is the label followed by the value.
- A Field names its control with aria-labelledby on the web and in the terminal, including RadioGroup and Slider.
- Menu items can be choices:
type: "radio"or"checkbox"withcheckedgives menuitemradio / menuitemcheckbox and a check mark. - A Dialog puts the first focus on the control marked
autofocus, else on the first control of its body or footer, not on Close. - Toasts:
keyreplaces a shown toast in place (a hovered or focused one stays paused),<Toaster :max>caps the pile, errors close after 10 seconds, Escape dismisses the newest. - A terminal toast is one row.
- Form shows the gap between its fields.
- Grid accepts attributes on its root and has
min-item-widthfor as many columns as fit. - New components: Pagination and NumberInput, on the web and in the terminal (a small NumberInput is one row there).
- Text on a filled control (primary and danger Button, Badge, Avatar, the current Pagination page) is readable in both modes:
--roots-on-primary,--roots-on-error,--roots-on-successand--roots-on-infoare derived from the theme, white where white reaches 4.5:1 and a dark ink elsewhere. The default green primary Button now has dark text. - A Table column's
comparegets the two rows as its third and fourth argument. - Tag remove buttons and Table sort buttons are at least 24 px.
- The terminal ScrollArea follows content that arrives later, starts at the top when its content is replaced, shows a focused field with its label, and can
fillits flex parent (fillworks on the web too). - A Slider or ProgressBar in a terminal flex row shares the row with its neighbours.
- A Badge inside a Link or Text keeps its padding in the terminal.
- A Select without v-model shows the value it was given.
- Inputs, Textareas, NumberInputs, Selects and secondary Buttons have a border of about 3:1 against the page; warning Tags and warning toast icons use the readable warning text colour.
Data
- In the browser, all tabs of an app now share one SQLite database: a second tab opens it at once and sees the first tab's data, where it used to wait 3 to 4 seconds and get an empty in-memory copy.
- Live queries and
db.onChangefollow writes made in other tabs. - A statement on its way to a tab that closes fails with D155; an open transaction of that kind is rolled back.
db.reasonsays why a database is not persistent:"no-opfs","another-tab"or"memory".db.live(() => [sql, params])follows the values the function reads and keeps the previous rows until the new result arrives;table.live(() => where)does the same.- A failing live query in development says so when the page never reads
.error, so a failure is not mistaken for an empty result. await db.settled()resolves when the live queries show every write made so far.roots db resetdeletes the local database and applies the migrations again; D132 now says how to start again while developing.roots db typesno longer marks a view's columns nullable when they come from NOT NULL columns through inner joins, and types anINTEGER PRIMARY KEY DESCcolumn (which accepts NULL) as nullable.- A live query whose parameters change right after a result arrived no longer keeps the old rows.
roots db migrate,queryandtypeswork in a project that has no entry component yet.- D163 and D164 now recommend
roots add @sqlite.org/sqlite-wasm@<pinned version>instead of npm. - Live queries with parameters are invalidated per table on the iOS shell (they re-ran on every write).
Theme, CSS and the terminal
- An app can switch between light and dark at run time:
theme.set("dark" | "light" | "system")fromroots/platform, withtheme.currentandtheme.resolvedas refs; the choice is remembered and shown before the first paint. - Dark theme variables and
dark:utilities also apply under<html data-theme="dark">, and not underdata-theme="light"; an app's own:root { --roots-… }still overrides them. roots/platformhasscreen:columns,rows,widthandheightas refs that follow resizes, andscreen.scrollTo.- The default text, muted, success, error and info colours pass 4.5:1 as text: success and info are darker, warning has a text colour of its own (
text-warning), dark mode has its own success, info and error. (The default primary is a brand colour and stays at 2.5:1 as link text; choose a darker:primaryfor links.) - Buttons and inputs have a border of about 3:1 against the background (
--roots-control-border). - The build warns (B168) with the measured ratio when a
:themetext colour is under 4.5:1 against its background, or when neither white nor dark text reaches 4.5:1 on a fill colour. - Images may be
data:andblob:URLs under the generated Content-Security-Policy, in the built page and underroots devandroots preview. - A terminal page taller than the screen scrolls: Page Up/Down, arrows, the wheel and the focus, with a scrollbar in a column of its own.
- In the terminal, a row's
items-centeranditems-endnow place Links, Buttons, Checkboxes and Inputs. - In the terminal, a
flex-1item inside a row that is as wide as its content keeps its content; it used to be cut to nothing. - The terminal uses the colours of
:theme :darkon a dark screen. - The terminal no longer warns about classes of the app's own CSS,
dom:classes, or the router'sr-link-active/r-link-exact-active; the link of the current route is bold. - The docs state that one spacing step is one row in the terminal and show the
term:idiom.
Dev server and launcher
roots devruns the link check ofroots build: a misspelt named import is B134 in the browser overlay and the terminal instead of a white page;roots dev --target termchecks the same before it starts the app.roots devshows the build's warnings in the page, in a panel that can be closed.roots devshows run-time errors in the page: errors nobody handled, rejected promises, and errors reported with theroots:errorevent, with the message, the stack and the component chain when the error has one. Production builds have no overlay.- A change that only alters
app.cssswaps the stylesheet without reloading the page. - The rebuild line of
roots devreports wall time from the change being seen to the pages being told, and what it is made of:rebuilt in 0.47s (build 0.32s, link check 0.08s): page reloaded. - A dev page whose reload channel was interrupted during a build reloads when it reconnects.
- New command
roots preview [--target web|pwa] [--port n]: builds for production and serves the result on localhost; the web build's summary now names it. bin/rootsstops withroots: the toolchain did not load: <subsystem>and exit status 1 when a part of the toolchain does not compile, instead of going on to "unknown command".bin/rootsno longer prints SBCL's compiler notes after a toolchain change;ROOTS_DEBUG=1shows them.
Examples
- The gallery, notes and hello-lisp examples use primaries that pass the new contrast check.
- The bookstore example is about 380 lines shorter: lazy routes,
redirect,beforeEnter, the router's scrolling and focus,db.live(() => …),db.settled(),db.reason, thethemeandscreenAPIs, toastkeyand NumberInput replace its workarounds; its README lists what was removed and the four workarounds that remain.
Behaviour changes (what an existing app may notice)
roots dev,roots previewandserve-staticsendimg-src 'self' data: blob:in the HTML CSP header (it had noimg-src, sodefault-src 'self'applied). A project:img-srclist replaces it.- DOM skin: Input, Textarea, NumberInput, Select trigger and secondary Button borders are darker (
--roots-control-border); warning Tag text and warning toast icon use--roots-warning-text. - Terminal layout: a row whose width comes from its content (a row inside a row, a row in a column with
items-start/center/end) is now as wide as the content of itsflex-1items; it used to count them as 0. roots build --target webprintsor preview it: roots preview.- A loading Button is no longer natively disabled: it stays focusable; pressing it does nothing.
- Error toasts close after 10 s by default (they used to stay);
duration: 0orcreateToaster({ errorDuration: 0 })keeps them. - Escape pressed anywhere dismisses the newest toast when nothing else handled the key.
- Dialog and Drawer first focus:
[autofocus], body, footer, Close, panel (was: body, else the first tabbable). - A modal's focus trap no longer pulls the focus back when it moved into another overlay.
- Layers: DOM pop-ups 1150/1151 (were 1000/1001); terminal pop-ups z-30/31 (were z-10), tooltip z-40, toasts z-50.
- The Select button gets
aria-labelledby(label + value) and always has an id; a Select in a Field without its ownlabelhas noaria-label. An uncontrolled Select shows its value. - Kit controls in a Field get
aria-labelledbyunless they have their own label; the Field label has an id; RadioGroup and Slider fields render nofor=; the Field root carriesdata-reveal. - Input, Textarea and Select focus: a solid 2px outline replaces the 30 % ring; an invalid Input shows the error border.
.r-formis a flex column, so its 1rem gap is visible.- Grid always has one root box; attributes land on it instead of warning R211.
- Table
compareis called with four arguments. - Tag remove buttons are at least 24×24 px; Table sort buttons at least 24 px high.
- Terminal: a toast is one borderless row; the Badge root is a Box; ScrollArea polls every 120 ms, clamps, and resets on replaced content; focus reveal includes the
data-revealancestor. - Additions:
ToastApi.dismissNewest(),Toaster.setMax(),<Toaster :max>,FieldContext.labelId(),setControl(node, labelable?), a third argument tocontrolAttrs, ScrollAreafill, GridminItemWidth, Pagination, NumberInput. - A route
componentthat is a function with zero arguments is a loader. A zero-argument setup function used directly as a page gets R408 with a hint to wrap it indefineComponent. createRouterthrows R403 for unknown route keys,:id?/:x+/:x(...)params, and*inside a longer path.- Web targets scroll by default (restore on Back/Forward,
#hash, top on a new page);history.scrollRestoration = "manual"while mounted. history.statealso carriesscroll; the first entry is stamped{position: 0}; eachpushdoes one extrareplaceState.- A history entry not made by the router gets
position + 1andnavigationType: "push". - A Back/Forward whose guard or loader throws puts the URL back.
- Function values in a
propsobject are no longer called as getters. - Route locations have
matchedRoutesandnavigationType. - R404 message: "A navigation was redirected more than 10 times".
- A child route's
metais a merged copy of parent and own meta. - In the browser a second tab shares the first tab's database (
persistenttrue, no D151). In-memory fallback remains without OPFS, Web Locks orBroadcastChannel, or next to a tab of an older build. db.onChangeand live queries fire for other tabs' writes.- All tabs share one SQLite connection: TEMP tables, ATTACH and hand-set PRAGMAs are shared and lost when the leading tab closes.
- New errors D155 and D173; changed messages D151, D154, D132, D140, D163, D164.
roots db types: view columns from NOT NULL columns through inner joins are not nullable.roots db migrate|query|types|resetwork without the entry component.EXPLAINruns with NULL for every placeholder (exact invalidation for parameterized live queries on the iOS plugin).- Driver interface additions:
reason,notify,subscribe,DriverOptions.setup; typesLiveSource,PersistenceReason. - Dark rules have new selectors (
:root[data-theme="dark"]and the media query under:root:not([data-theme="light"]));dark:utilities use:where(). text-warningreads--roots-warning-text; basebuttonandinputborders use--roots-control-border.- Default success and info are darker in light mode; dark mode has its own success, info and error.
- New build warning B168 (not an error under
--strict). - Every DOM page and the extension manifest get
img-src 'self' data: blob:unless roots.lisp lists:img-src. theme.jsand one blocking<script>appear only in apps that importtheme.- Terminal: a page taller than the screen scrolls with a scrollbar in the last column; Page Up/Down, wheel, and arrows/Home/End without focus have default actions there.
- Terminal: in a row with
items-centeroritems-end, Button, Input, Checkbox, Link and Image follow it. - Terminal: a dark screen applies the colours of
:theme :dark. - Terminal: no R361 for
dom:/mobile:classes, app CSS classes,r-link-active;r-link-exact-activeis bold. - The terminal bootstrap imports the backend namespace and calls
setDarkTheme?./setAppClasses?.; the terminal platform adapter importsruntime/term/display.lisp. - R504 for an invalid
theme.setvalue. roots devfails a build that does not link (B134, B132, B135 …);--target termdoes not start Node on a link error./__roots/events:okdata is{"build": n, "warnings": [...]}; newcssevent.- A style-only change swaps the stylesheet without a reload.
- Dev pages are served with the client
<script>ahead of the first other<script>. - Run-time errors and build warnings draw panels in dev pages.
- The rebuild line has a new format and includes the snapshot and broadcast time.
bin/rootsexits 1 when a present subsystem fails to load; compiler notes are hidden withoutROOTS_DEBUG.- New command
roots preview. - Router: the scroll position of the entry on screen is written to
history.state150 ms after the window stopped scrolling (one morereplaceStateper scroll gesture) and onvisibilitychange→ hidden, not only onpagehide. - Router: a Back/Forward that ends without a navigation always returns the history to the entry on screen (also after a refused redirect, two Backs under a pending guard, a superseded lazy page). A Back whose guard redirects to the current route goes forward again instead of rewriting the entry gone back to.
- Router: an alias that lacks a param of its route is R403 at
createRouter(it was accepted and left the prop empty). - Kit: text on a filled control is
--roots-on-primary / -on-error / -on-success / -on-info, not--r-on-accent. With the default primary#42b883the primary Button, Badge, Avatar and current Pagination page now have dark text (#111827) where they had white; in dark mode the danger Button and the error, success and info Badges have dark text. An app's own--r-on-accentno longer reaches these (set--roots-on-primaryand its siblings);--r-on-accentremains the fallback in a stylesheet without the new variables. - Kit: a Select, Menu or Popover that is open when a Dialog or Drawer opens lies under the dialog and its backdrop until the dialog closes (DOM z-index 1000/1001, terminal z-10/11; modifier
underon the partsdismissandlist/panel). - Kit: a toast replaced through
keywhile hovered or focused does not start its time until the pointer or the focus leaves. - Kit: a
size="sm"NumberInput is one borderless row in the terminal; on the DOM its input has the extra classr-number-input__input--sm. - Theme: the dark variable rules are
:root:where([data-theme="dark"])and:root:where(:not([data-theme="light"]))(the weight of:root, as the single media rule of 0.3.2): an app's own:root { --roots-bg: … }wins in both modes again. - Theme: four more variables per mode (
--roots-on-*); B168 also reports a fill colour on which neither white nor#111827reaches 4.5:1. - Theme: the
themeAPI reads, writes and listens only from its first use; an app that never uses it keeps its owndata-themeunderroots devtoo. - Terminal: while a page scrolls, the app is laid out one column narrower (79 of 80);
screen.columnsstays the terminal's width. - Data:
roots db typestypes anINTEGER PRIMARY KEY DESCcolumn as nullable, in the table's Row type and in views (it wasnumber), and does not treat it as optional-on-insert rowid alias.
0.3.3: rendering, speed round ten #
Measured on the same machine as 0.3.0 (Intel Core i9-9900K, macOS, Node 24.13, Chromium 143, Firefox 144) on 10 and 11 November 2026, against the frozen trees of 0.3.2 and 0.3.0, with an A/A control in every session. docs/performance.md is rewritten for this release: the field tables (eight other frameworks, ten other signal libraries, versions stated, release candidates marked), what made the difference, and where Roots is not the fastest.
- Rendering script time 1.2× over 0.3.2 and 0.3.0 on the js-framework-benchmark operations (geometric mean 1.20 and 1.21, controls 0.99 and 1.01): creating rows 1.29–1.40×, swap 1.45–1.61×, remove 1.19–1.35×, replace 1.14–1.16×; partial update, select and clear are not resolved, and totals with style and layout show no difference (1.01–1.03). These are the gains of the benchmark's row; rows with a component or a nested list gain 1.02–1.10 on creation. Against Vue 3.5.44: script 3.26× and 3.35× (2.89× at 0.3.0), totals 1.29× and 1.34×.
- Against the field: the lowest script time on the geometric mean of the nine operations (Vue Vapor 3.6.0-rc.14 1.08 and 1.12 behind, Inferno 1.38, Solid 1.38–1.40, ivi 1.51, Svelte 5 1.65, lit-html 1.76, Preact 6.94), level with Solid on creating rows, where 0.3.0 was behind. The reactive core of 0.3.1, measured again: ahead of Vue 3.6 rc (1.13), Preact signals (1.30), alien-signals (1.36) and six more on the geometric mean of the 30 cases; 1.70× and 1.72× Vue 3.5.
- What changed in the runtime: template prototypes are kept in an inert document (Chromium and WebKit; create 1.21, create 10,000 1.17, append 1.11, replace 1.07 against the same tree without it); a list update is a scan over the previous and the new key arrays (swap: 2,068 → 1,073 function calls); compiled templates call lean helpers for plain elements of static templates (
setElText,bindElText,bindClassEl,onEl); handlers inv-forrows are shared (rowHandler,onRow); a class that asks acreateSelectorresult is written without a render effect (bindSelected); the list's record is the item's dependency. Per 1,000 rows of the benchmark app: 130,060 → 74,061 function calls, 22.9 → 16.0 live objects and 786 → 560 B per row; the heap after 1,000 rows is 2.08 MB (2.28). - Minifier: no
;before}, merged declarations, unbraced single statements,!0/!1,void 0, no description on symbols that are only keys. Bundles are within 1.3–1.7% ofesbuild --minifyon the same text. Raw / gzip -9: hello 51,443 / 16,505 → 45,553 / 15,587, bench app 62,525 / 20,177 → 58,672 / 20,038, todo 74,651 / 24,113 → 70,379 / 24,172, gallery 156,691 / 45,996 → 147,828 / 45,674.onis split fromonWith(event modifiers), and bundles for DOM targets defineCLONE_BACKEND. roots build --error-codes(opt-in;:error-codes tin roots.lisp; bundles only): runtime messages become their code ([R208]), 1.1–1.6 KB gzip less per app.error.code, the handler's code argument and the component trace are unchanged.- Behaviour changes (the full list is in docs/performance.md, "Semantics notes"):
- A fresh copy of a static template belongs to an inert document until it is inserted, in Chromium and WebKit; in Firefox it belongs to the page's. Until insertion
a.href,img.srcandform.actionanswer the unresolved attribute, and an<img>with a staticsrcstarts loading at insertion. Templates with a custom element, a<video>or an<audio>are not moved. - The
v-foritem is the list's record, not ashallowRef:.valueis tracked and read-only,isRef(item)is false. Assigning av-foralias or a slot prop in a template (n++,row = x) is compile error C254;row.done = trueworks as before. - A kept item is compared with
===when the item function takes one parameter (0 and −0 under one key no longer re-run its readers). - Handlers of static-template nodes in
v-forrows are one function per site and render, called with (row, event); a function-expression handler is evaluated per event. - The selector class binding applies only to
const name = createSelector(…)declared in the component's<script setup>; every other callee is bound as in 0.3.2. New binding kind:selectorin the template compiler;v-modelon it is C216. - Modules compiled for DOM targets need a runtime that exports the new helpers; the terminal target emits what 0.3.2 did. Lisp
defcomponenttemplates emit$.onWithfor an event with modifiers. - R307 and R308 are raised through the DOM backend's
warn(same text and console output, still not through thewarnHandler). - A
const x = Symbol("roots.…")that a chunk does not export and uses only as a key loses its description in a minified bundle. - A custom backend without
cloneNode,firstChildandisElementneeds--no-bundle. New optional backend methods:home,listenElement. - A list record is 84 B (was 72 B) and a list keeps two more arrays; the loaded bench app's heap is 1.32 MB (1.29).
- A fresh copy of a static template belongs to an inert document until it is inserted, in Chromium and WebKit; in Firefox it belongs to the page's. Until insertion
- Measured and not shipped: a compile hint on bundles (the todo example renders 8–9% earlier, the bench app 7–9% later, nothing on the gallery); lazy keys in
v-for(not needed). - Known gaps: Vue Vapor 3.6 rc is faster on select (0.89–0.94) and 4% ahead in totals with style and layout; Vue 3.6 rc's reactivity is faster in five of the 30 cases, and
dynamic: simple component (pull)is about 3% slower than in 0.3.0; three template patterns are behind Vue 3.5 (text/fill,attr/updateAll,if/updateAll); in Firefox replace is behind Solid and the totals of swap and remove are 12–15% above the others'; select in Firefox against 0.3.2 is not resolved; Solid, Preact, lit-html, Inferno and ivi ship 4–10 KB gzip against 19.6 and render first in about 0.6× of the time; the todo bundle is 59 B gzip larger than in 0.3.2; an imported or composable-provided selector does not get the selector binding.
0.3.2: security hardening #
A security audit of the whole tree (dev server, package manager, templates and runtime, native shells and targets, build and bundler, data), every finding checked by a second reviewer, then fixed and re-tested. After verification: one critical, four high and eight medium findings; all are fixed by default except one (roots.lock approvals, below). Each fix has a regression test. Several defaults change; each has a documented way back.
- A package's Lisp no longer runs in the toolchain. A dependency that resolved to a
.lispfile, or a.vuewith<script lang="lisp">, was compiled like app code, and compiling Lisp runs its macros: any package could run Common Lisp as the developer on everyroots buildandroots dev. It is now B129, with no opt-in; while a package file is compiled the compiler also refuses(lisp …),defmacroandmacrolet. Importing intonode_modulesby path, and a package directory that links outside the project, are B125. docs/build.md, "What a build trusts", says what a build trusts (your sources: completely; packages: not at all). - Minifier: a
varin a block or catch clause could get the short name of a binding around it, which changed what the program did (the audit's example made anisAllowedcheck pass for a forged token). The scope model now records where hoisted variables are written; the bundler's own renaming had the same hole. A catch clause with alet/constin its body and an unused parameter minified to code that did not load. The minifier's output is now compared with its input scope by scope before it is written. Short names differ from 0.3.0 for such code;catch (e) { var e }keeps its name (B137 when it cannot). - Output directory:
roots buildwrote and deleted through symbolic links inside it, so a checkout could carry a link that made a build overwrite files elsewhere. Every write and delete now walks the path without following links (B141), also for the dev server's error page.public/,icon.png,migrations/andnative/<platform>/are never read through a link (B163), and more key material is kept out ofpublic/(B164). - Package manager: OpenSSL, zlib and SQLite are loaded by absolute path on macOS (a library in the working directory used to be picked up by
roots why); a symlinkednode_modules/.rootsno longer makesroots installdelete the link's target (PM152); dependency names are validated everywhere ("../../src": "npm:x"deleted directories; PM138); a git, URL or file entry of roots.lock must resolve where package.json says (PM142). Also: no heap exhaustion from a 25-byte gzip, Authorization stays on its origin, the request line cannot be extended, project.npmrcfiles are not trusted with the environment, loopback is an address, names differing in case are refused (PM153). Not closed by default: a change to roots.lock alone can still approve an install script or pin a version inside the cooldown;:packages (:builds (…))and:verify-age t(orroots install --verify-age) close it for projects that set them. - SQLite WASM is pinned by hash. roots/data shipped
index.mjsandsqlite3.wasmfrom whatevernode_moduleswas found above the project. The build now ships them only when their SHA-256 is the pinned one (D166); another version is an error (D164, was a warning) unless roots.lisp pins it under the new:datakey. - PWA updates are checked. The service worker fetched its files through the HTTP cache and never compared them with the build, so an update could be assembled from stale files. Every file is now fetched past the cache with its SHA-256; a failed install keeps the running version and reports through the new
updates.onError. Two apps on one origin no longer delete each other's caches. - Templates and the DOM backend: the attribute rules were case-sensitive, so
onClick,srcDocorHREFfrom data (a spread, fallthrough attributes,<component :is>props) got past them. Names are now judged in lower case, once per name (a Map lookup per write in place of a chain of string comparisons).<component :is>refuses strings naming document-level tags (R306), the router never renders a link out of the app, route params and query have no prototype, and invalid attribute names are dropped (R308). - Trusted Types. Pages of web, pwa and the Roots shells are written with
require-trusted-types-for 'script'; the only policies are the runtime's own for the service worker and the roots/data worker URL.:cspin roots.lisp adds origins to the policy (never toscript-src) and:csp (:trusted-types nil)turns the requirement off. - Dev server: bounded request heads, responses and connection slots; at most 32 reload channels, for the page only; no socket is written while the server lock is held; no framing by other origins;
Hostneeds the port; internal errors during a rebuild no longer endroots dev. - roots/data:
roots db typesandmigrate --checkcould write arbitrary files through a migration (ATTACH); toolchain connections cannot attach now, and a migration withBEGIN/COMMIT/ROLLBACKis refused before anything runs (D136). At run time ATTACH needs{ attach: true }(D117),trusted_schemais off, a wrong parameter count (D113), a second statement (D114), NaN and NUL are refused on every driver, integers beyond 2^53 come back as decimal strings, live queries see writes made by triggers and by failing statements, androots db querycannot repaint the terminal. The WASM worker ran a statement's stale error instead of the statement after a failure; fixed. - Native shells: the plugin allow-list is what the app imports (it was all five built-ins), app data is excluded from backups unless
:native (:backup t), the sqlite plugin refuses ATTACH and VACUUM INTO, the bridge checks its ABI version and answers frames withBRIDGE_UNAVAILABLE, script cannot leave the app without a user gesture, logs name scheme and host only. Android and iOS code was compiled and unit-tested; nothing ran on a device. - Parser and build errors: a name declared twice is C110,
<!--in a module C111, deep nesting C109 or B192, a.jssyntax error one line with file and position, anything unexpected B191 (ROOTS_DEBUG=1for the backtrace).import "pkg"keeps the module in a bundle whatever the package'ssideEffectssays. - New codes: B129, B137, B158, B167, B191, B192; C110, C111; R308, R407; PM138, PM153, PM163; D113, D114, D117, D136, D154, D166. New roots.lisp keys:
:csp,:data,:pwa (:precache-exclude),:native (:backup),:packages (:builds :verify-age). Environment:ROOTS_NO_CACHE,ROOTS_DEBUG,ROOTS_OPENSSL_DIR. - Tests: 472 in the
…/testssystems (471 pass here;e2e-extension-popupcannot load an extension on this machine), 64 in the package manager's, 39 in roots/data's (the extension e2e fails the same way). roots-build's tests now load roots-pm. - Open: the roots.lock default above; an intermittent SBCL abort in the package manager's test suite under load, cause unknown; device runs of the shell changes; no row limit in roots/data; cache entries and the store index are not authenticated.
0.3.1: the reactive core, speed round nine #
Measured on the same machine as 0.3.0 (Intel Core i9-9900K, macOS, Node 24.13), against the frozen 0.3.0 tree with an A/A control in every run (ab-reactivity.mjs, 4–6 processes, both load orders).
- Reactive core 1.2× faster than 0.3.0 (geometric mean of the 30 cases: 1.19 / 1.21 / 1.22 over the processes, control 0.99 / 0.99 / 1.01); 25–27 of the 30 cases lie outside the control, none is resolved slower (
dynamic: simple component (pull)reads 0.95–0.99, not resolved). Against@vue/reactivity3.5.44: 1.69× and 1.75× in the two load orders (1.44–1.45 at 0.3.0);create 1to1000is level with Vue (0.96× / 1.00×),create 1to1ahead. - What changed: the graph functions test links and nodes with
!== nullinstead of truthiness (V8's optimized code for the hot functions is 17–44% smaller);EffectBaseassigns its fields in the constructor, so creating an effect no longer goes through a genericConstructcall; an effect whose function declaresonCleanupkeeps its registrar in a field instead of a WeakMap (creating such an effect is 4–5× faster, a write that re-runs it 1.2–1.4×). Dropped because their gates were not met: two ref-setter variants and three variants of the validation walk. - Dialect:
define-classtakes(:constructor-fields t)(fields are assigned at the start of the constructor in declaration order; no class-field syntax; TypeScript output declares them withdeclare);(some? x)and(none? x)compile tox !== null/x === null. No existing compiled output changed (120 sources byte-identical). - Behaviour at the edges: a
ReactiveEffectis 96 B (was 88; one declaringonCleanup144 B, was 226); itsscheduleris fixed at construction (nullcounts as none); new exports for list bindings:trackDep,triggerDep,selectorBind,selectorUnbind.onCleanupand R106 behave as documented. - Benchmarks: seven more frameworks behind
run-render.mjs --frameworks(Solid, Svelte 5, Preact, ivi, lit-html, Inferno, Vue Vapor: ports of their js-framework-benchmark entries) and eight more signal libraries behindrun-reactivity.mjs --libs; a hand-written app that clones rows from a<template>(vanillatpl, a lower floor thanvanillaon creation); two cases for effects that declareonCleanup;--presweep,--browser firefox;run-startup.mjsaccepts the field apps.
0.3.0: production builds, and speed rounds four to eight #
Numbers are from 10 November 2026 on one machine (Intel Core i9-9900K, macOS, Node 24.13, Chromium 143, Vue 3.5.44), which was under load (load average 5–27 on 16 threads): every comparison of two builds was taken with an A/A control in the same session, and a ratio inside the control's range is given as "no difference shown". The numbers of 0.1.4–0.2.1 were measured on another machine and are not comparable in milliseconds. Method, tables and what is still slower: docs/performance.md.
- Production builds (M13):
roots buildbundles and minifies by default. The bundler, tree shaker and minifier are written in Lisp (src/bundle/, systemroots-bundle, docs/bundling.md): one ES module per entry, tree-shaken at the granularity of top-level statements (sideEffectsof packages,/* @__PURE__ */and/* @__NO_SIDE_EFFECTS__ */annotations, a pure-expression grammar), defines and constant folding (process.env.NODE_ENV, the runtime'sDEV), scope hoisting with ESM's semantics kept (live bindings, namespace objects, cycles), deterministic output. The minifier prints the parser's token stream back: whitespace and comments go (licenses stay), bindings get short names; no expression rewriting, no property mangling; its output is parsed again before it is written.--no-bundlekeeps one file per module,--no-minifya readable bundle;:bundle/:minifyin roots.lisp, also per target.roots devandroots runnever bundle. - Size: the benchmark app ships 19.3 KB gzip in one file (59.3 KB raw); 0.2.1 shipped 41.4 KB gzip in 5 files, Vue's build of the same app is 43.7 KB. esbuild
--bundle --minifyover the same modules: 19.3 KB gzip, 53.7 KB raw. The todo example: 23.1 KB gzip (71.4 KB raw) against 62.5 KB in 16 files for this release's own--no-bundleoutput (0.2.1's build of it was not measured). - Code splitting, as far as it goes: a dynamic
import("./x.js")target gets its own entry file, modules shared between entries go tochunks/<name>-<hash8>.js(preloaded with SRI when the page imports them statically), and the roots/data worker is a bundle of its own. Routes are not split automatically, there is no chunk configuration, and there are no source maps yet (MILESTONES.md, M13). - Bundler diagnostics: B128 (a CommonJS package module), B132 (an import cycle that reads a lexical binding before its module ran), B133 (warning:
import.meta.urloutsidenew URL("./…", import.meta.url), or such a URL leaving the output directory), B134 (warning forns.missing; an error for a named import its module does not export), B135 (a namespace member written), B136 (a script where a module was expected). - Parser: annotation comments on the AST, a scope tree for whole modules, a CommonJS test and position-based edit plans (docs/parser.md). Lisp dialect:
(declaim (pure f))and(pure (f …));$.defineComponent, hoisted templates anddefcomponentare marked pure, so an app that imports one kit component does not carry the others. DEVis false inroots build(bundled or not): prop validation (R202,R203,validatorcalls), the R215 scan of pure insertions and the R224 check run inroots devandroots runonly. Prop validation and the R215 scan ran in every build before; the R224 check is new and development-only.process.env.NODE_ENVis a build-time literal in every build, not only in the bundle (0.2.1 never replaced it):"production"inroots build, bundled or not,"development"inroots devandroots run. It is replaced in npm packages on every target and in app code on DOM targets; app code ontermkeeps the realprocess.env, and it is never replaced in the runtime.roots runandroots dev --target termwrite a development build (unbundled,DEVtrue,NODE_ENV"development") intodist/term, where 0.2.1 left the same output asroots build --target term. It replaces a production build in that directory: runroots build --target termagain before shipping.- Unbundled output (
--no-bundle,roots dev; DOM targets): amodulepreloadlink with SRI per statically imported module and the compile hint//# allFunctionsCalledOnLoadon the modules the toolchain compiled (runtime,.vue,.ts,.lisp; under 128 KB; not the bootstrap, plain.jsfiles or npm packages). - First render (bench app, from the entry script's request): the bundle 13.3 ms on localhost against 15.3 ms unbundled with links and hints, 18.5 ms without them (the 0.2.1 layout) and Vue's 24.4 ms; over 25 ms latency and 10 Mbit/s 56.5 ms against 89.0, 129.8 and Vue's 110.0 ms. Todo example: 23.1 / 31.6 / 42.8 ms on localhost, 73.1 / 124.8 / 175.9 ms over that network. All outside their A/A bands (0.89–1.12 at the widest).
- Speed, rounds four and five (the runtime's per-row work): an interpolation alone in its element is the element's text content; registering a listener allocates nothing (backend
listen); the template walk is one backend call (collect); a list has one owner and each item is a scope, not an owner; render effects are smaller; kept rows are not touched by an update; the key's text is written once ($.setText); a swap needs no key map. Reactive core: one pass over the flush queue,effect(fn, { cleanup: "run" }), scopes that do not own computeds and release child scopes in O(1). - Speed, round six was measurement: the todo example, the gallery, kit widgets and startup instead of the benchmark row. It found that no compiled Roots component reached the static-template path (they are written with primitives), that every built app ran its development checks, and that the unbundled layout was slower to first render than Vue over a network. New tools:
run-startup.mjs,run-patterns.mjs,run-widgets.mjs,ab-reactivity.mjs, with an A/A control in each (runtime/bench/README.md). - Speed, round seven (what real components are made of):
Box,Text,Button,ImageandSpacergo into static templates; adjacent text and interpolations are one binding; component instances allocate less (lazy hook lists and$refs, one shared empty attrs view, shared Proxy handlers, node state in a symbol-keyed property); a bound attribute's first "absent" value is not written; bindings that read nothing reactive are released; a kit part without pass-through uses its skin's class string directly; a scope stops its effects without iterators. - Speed, round eight: the measurements. Against Vue 3.5.44, rendering script time 2.89× (geometric mean of the 9 js-framework-benchmark operations, faster on all 9; a second session 2.83×), totals with style and layout 1.29× (1.28×, 8 of 9 in the second session); the reactive core 1.39× with Roots loaded first and 1.41× with Vue loaded first (30 cases). Template patterns of real components (7 patterns × 4 operations): 1.78× Vue, 20 of 28 cells outside the A/A band (0.76–1.32), all faster. Heap after 1,000 rows 2.28 MB (0.2.1: 2.84 MB, Vue 3.48 MB), 24.4 live objects per row (Vue 48.9).
- Against 0.2.1: the reactive core is 1.05× faster (geometric mean of 31 cases, 1.037–1.056 over 6 processes; A/A 0.980–1.020). On the rendering benchmark script time reads 1.14× with an A/A of 1.12× and totals 0.99×: no difference shown in time on that row, which the 0.1.x rounds had already worked on. Against the tree after round five, mounting 100 kit widgets or components is 1.27–1.89× faster (11 kinds, A/A 0.93–1.08); the 9 component kinds keep 21–52% fewer live objects per instance (a bare
Box12%, a barediv6%), and the template patterns are 1.25× faster (geometric mean; 11 of 28 cells outside the band, 10 of them faster: creating primitive rows 1.59×, text rows 2.33×; the eleventh is the control cell itself). - Known gaps: the reactive core is slower than Vue on
create 1to1(0.81–0.82×) andcreate 1to1000(0.66–0.67×; thefn.lengthread that keepsonCleanup's promise, and a wider gap on this machine than on the earlier one, with no change in Roots on these cases since 0.2.1). In the patterns benchmark three medians are below Vue's:text/fill0.82×,if/updateAll0.85×,attr/updateAll0.92× (script; inside that session's A/A band), and with style and layout Vue's median is lower on 7 of the 28 cells by 2–8% (five of the seven fills,if/updateAll,attr/updateAll; no A/A band was computed for totals). On the rendering benchmark two cells read below their session's control: clear 1,000 rows is 0.93× against 0.2.1 in script and in totals (12.0 against 11.1 ms; control 0.95–1.67 and 0.94–1.07, A/A of that cell 0.95), a possible regression of under 1 ms that these sessions neither show nor exclude; and in the second, more loaded session Vue's total for create 1,000 rows was lower (0.93×, spreads ±16–18%; 1.10× in the first session). The bundle has no compile hint: after its response arrived, the todo bundle needs 19.6 ms to the first render where the hinted unbundled modules need 4.9 ms (it still renders first in total). The 2× goal for the reactive core is not met. - Behaviour changes (docs/performance.md, "Semantics notes"): inside a
v-foritemgetCurrentScope()returns the item's record (aScopewithoutrun/stop); props are validated in development builds only and adefaultfactory runs at the first read; a text run is one text node, and an interpolation that throws takes its run with it; a bound attribute's firstnull/undefined/removingfalseis not written (bindAttr(node, name, getter, over?)for hand-written render code); components without attrs share one emptyattrsobject;props/attrsviews throwR204ondefineProperty,setPrototypeOfandpreventExtensionstoo; the sharedonCleanupthrowsR106insideuntrack, a computed's getter and awatchgetter; anEffectScopedoes not stop the computeds created in it; a binding that read nothing reactive in its first run is released and stopped (the effects of blocks,modelandLinknever are); a scope that released an effect stops its remaining effects, and a parent scope whose child stopped on its own its remaining children, in no particular order; a bound class whose first value is empty no longer claims the first position among its node's class sources ("x danger", not"danger x"); new codesR224,R225. New optional backend methods:isElement,collect,setTextContent,listen,adopt. Two new requirements for custom backends: a node fresh fromcreateElementhas every unset prop absent (typeandaria-hiddenexcepted), and host nodes get an own property keyed bySymbol("roots.nodeState")(a non-extensible node gets a WeakMap entry instead). - Compiler: the variables a
loopgenerates are numbered from the second loop of a compilation unit on (result2,end2;repeatcounts in_i), so two loops in one function no longer redeclare them. - macOS: the package manager's socket constants come from the platform (
EAGAIN,SOL_SOCKET,SO_ERROR; noMSG_NOSIGNALon the BSDs); a lockfile that lists install scripts its tarball does not have (fsevents, recorded from registry metadata on Linux) warns PM123 and uses the tarball's instead of failing PM146; a new--outdirectory is compared by the canonical path of its longest existing ancestor (/var→/private/var), so B141 judges it correctly; npm package lookup during a build compares canonical paths too, so packages resolve for a project under a symlinked path; the single binary's extracted cache is locked after the rename (macOS refuses to rename a read-only directory). - Package manager: the update review shows a version that joins the ones still in the graph as an addition (
+ semver@6.3.0 (next to 7.1.0)), androots add alias@npm:realchecksreal, not the alias. - Benchmark tools (runtime/bench/README.md): besides the four above,
profile-render.mjs,profile-reactivity.mjs,count-dom.mjs,heap-objects.mjs;run-render.mjs --baseline <checkout> --aa,run-reactivity.mjs --order. - Tests: 454 tests / 5813 assertions (0.2.0: 328 / 4642);
roots-bundle/testsalone has 77. In the run for this release, on the machine above, 452 passed, 11 were pending for tools it lacks (Gradle,@sqlite.org/sqlite-wasm, network namespaces) and 2 failed:e2e-extension-popup(that Chromium blocks the extension page, ERR_BLOCKED_BY_CLIENT) anddata-runtime-conformance-bun(6 of its 20 cases: SQLITE_BUSY on commit under the installed Bun'sbun:sqlite; not investigated). - README has a banner; docs/performance.md, MILESTONES.md and the presentation describe 0.3.
0.2.1: its own repository #
- Roots moved to github.com/marcuspuchalla/roots, with its history (the plan, the
unified/beginnings and every release up to 0.2.0); it started inside the vuecli repository, which keeps the Vue terminal renderer.docs/PLAN.mdmoved along. - Paths are relative to the repository root:
bin/roots,node runtime/bench/…,sbcl --script docs/presentation/build.lisp; the tests look for their oracles in this repository'snode_modules, not the parent directory's. - Development dependencies are declared in
package.jsonand installed with Roots's own package manager (bin/roots install,roots.lock): tsc, Node types, acorn, semver, the parser corpus (zod and ajv ship their TypeScript sources) and Vue, Vite and esbuild for the benchmarks. The 7-day cooldown held back semver 7.8.6, Vue 3.5.45 and Vite 8.3.6; the pins are the newest versions older than a week.examples/notesinstalls the same way. - Parser corpus: without vuecli's sources the default run samples up to 500 TypeScript files from
node_modules(was 150): 432/432 strippable files byte-identical to Node; the full run 1193/1193. - MIT license file;
.gitignorefornode_modules/anddist/.
0.2.0: SQLite, UI kit, native shells, package manager, speed #
0.2.0 releases the 0.1.x line together. Everything 0.1 announced as "not in 0.1" is in, and 328 tests / 4642 assertions pass:
- roots/data (0.1.1): SQLite on every target (node:sqlite / bun:sqlite in the terminal, SQLite WASM with OPFS in a Worker on the web, PWA and extension, the native shells on phones, sb-alien in the toolchain); checksummed migrations, live queries,
roots db migrate | types. - roots/ui (0.1.2): 38 tier-1 components on headless state machines with WAI-ARIA keyboard tables, a DOM skin and a terminal skin, pass-through props and an unstyled mode, overlay layers.
- Own native shells and one binary (0.1.3): Kotlin + WebView and Swift + WKWebView, generated by Roots, with the
RootsNativebridge (ABI 1) and allow-listed plugins; Capacitor stays selectable.roots self binarywrites the whole toolchain as one executable. - Package manager (0.1.5):
roots add | install | …on its own TLS client, a 7-day cooldown by default, no install scripts, a tamper-evident store, the lockfile verified as untrusted input. - Speed (0.1.4, 0.1.7, 0.1.8): against Vue 3.5, rendering script time 2.06× (faster on all 9 js-framework-benchmark operations), totals 1.17×; the reactive core 1.41× (geometric mean of 30 cases). The 2–4× goal is met for rendering script time, not yet for the reactive core (docs/performance.md).
- A presentation of the framework:
docs/presentation/(reveal.js), built bysbcl --script docs/presentation/build.lispfrom the real sources, tests and benchmark tables;presentation-buildschecks that every quoted file, range and table still resolves. - README and MILESTONES describe 0.2; the road to 0.3 is in MILESTONES.md.
0.1.8: speed, round three #
- Benchmark method:
run-reactivity.mjsempties the young generation (gc({type:"minor"})) before every sample, outside the timed part. The creation cases' medians had followed the sample order: the library sampled first took the scavenge the previous pair left behind (docs/performance.md § Method). - Reactivity: effects carry 7 fields instead of 10 (R102 counted per flush with a mark and a map that exists only while effects re-run; an effect's own
onCleanupregistrar lives in a WeakMap); building a graph skips the duplicate-link search for deps without subscribers; alternating reads (a, b, a) are found inlinkwithout a call; connect/disconnect are lighter. 1.41×@vue/reactivity3.5 (geometric mean of 30 cases over three whole-suite runs: 1.39, 1.42, 1.41);unstable1.1× (was below parity),create 1000to11.2×,create 1to1andcreate+disposeat parity. Rendering unchanged: script 2.06×, totals 1.17×. - Known gap:
create 1to1000is 0.75–0.83×:effect()readsfn.length(~10 ns) so that a declaredonCleanupparameter can be called after the run, as R106 promises. The semantics are kept; Vue'sonEffectCleanupmodel is the documented alternative.
0.1.7: speed, round two #
createSelector(source)(roots/runtime):isSelected(key)with O(1) updates; only the readers of the old and the new key re-run, and keys leave with their last reader.- Event delegation in the DOM backend: the common bubbling events (click, input, keydown, pointer and mouse events, focusin/out …) go through one listener per document, in
composedPath()order withcurrentTargetset andstopPropagation()honoured; non-bubbling, touch, wheel, change and submit stay native. Differences from per-element listeners are listed in docs/runtime.md §6. - Compiler: module-level
defvar/defparameteremitvar(no temporal-dead-zone check on every access); inside functions they staylet. - A computed that reads itself while computing always reports R101 (a connected computed used to return its stale value).
- Performance: computeds are their own dependency node; aligned field layout; batches, try/finally and stack walks only where needed; lazy scope lists; template walk programs; smaller render effects. Reactivity 1.43×
@vue/reactivity3.5 (geometric mean of 30 cases; 0.1.4: 1.31×), rendering script time 2.08× Vue 3.5, faster on all 9 operations, totals 1.18× (docs/performance.md).
0.1.6: integration fixes #
- B157 (warning): a target's
:pluginslists Capacitor npm plugins while the target uses the Roots shell, which ignores them; the hint points to:native :pluginsor(:shell :capacitor). - examples/notes uses the Roots shell's native
sqliteplugin on phones (the Capacitor plugin list moved into a comment showing the Capacitor setup); its builds are warning-free.
0.1.5: the package manager (roots add / install) #
- Package manager (M10):
roots add | install | remove | update | why | outdated | approve-builds | store verify | import-lock, written in Lisp on its own OpenSSL 3 TLS client (certificate and host-name verification always on, TLS ≥ 1.2, CONNECT proxies). For vue, express and vite it installs exactly the packages npm installs, faster cold and warm. - 7-day cooldown by default: versions younger than the window are skipped; exemptions need a
:reasoninroots.lisp :packages;--allow-youngis recorded inroots.lock; PM131 names the newest eligible version when nothing fits. - No dependency code runs at install:
roots approve-buildspins name, version, integrity and source; unapproved scripts are an error in CI; approved scripts run without secrets, with a temporary HOME, a timeout and no network. - The lockfile is untrusted input:
roots.lockis deterministic text, re-checked on every install (registry hosts, integrity, name/version/dependencies against each tarball's package.json);--frozenis byte for byte; a warm store installs with zero network. - Tamper-evident store: read-only, content-addressed files, re-hashed before linking into an isolated pnpm-style
node_modules;roots store verifyre-hashes everything. - Build: apps import declared npm packages (
exports→module→main, emitted undernpm/<name>@<version>/); an undeclared import is an error naming how to add it (B121/B123). Package code is read, never run. - Supply-chain checks: update review with red flags, typo and new-package warnings on
roots add,roots publish --dry-run(staged + OIDC),roots doctor --ciworkflow checks. - Semver: node-semver's full range grammar; 0 mismatches against the real package on 155,232 pairs. Docs:
docs/packages.md(guide, security model, PM1xx codes).
0.1.4: speed, round one #
- Reactive core rewritten internally: doubly-linked dependency links reused in read order, no recursion when propagating or validating (a 20,000-deep chain is tested), lazy subscription for computeds, no allocation on hot paths. About 1.3× faster than
@vue/reactivity3.5 on average over 30 cases (up to 2.1×), from 1.6–3.8× slower. No API change. - Static template cloning: subtrees of plain HTML elements compile to a hoisted
$.templatespec cloned with onecloneNode(new helperstemplate,clone,bindText,insert; optional backend memberscloneNode,firstChild,clear, CONTRACT §3/§5). The terminal builds the same nodes with the normal helpers. - Render effects: one object per binding, re-run only when a source changed, queued without allocating;
:class="{…}"compiles to a class string. - Keyed
v-for: common prefix/suffix kept in place, single insertions/removals without a key map, LIS-based moves, one-operation clear. - Result: rendering does about 2× less script work than Vue 3.5 on the js-framework-benchmark operations and is faster on every one; heap after 1,000 rows 2.85 MB (Vue 3.53 MB).
- Compiler: numeric
defconstants are inlined, a leadingthisparameter typesthisin TypeScript, a newfor-inform. - Benchmarks:
runtime/bench/run-reactivity.mjsandrun-render.mjs(Chromium, vs Vue 3.5 and vanilla), described indocs/performance.md. - Behaviour: new error R106 (a shared
onCleanupcalled after its effect's run); a failing sync scheduler no longer stops the other schedulers of the same write.
0.1.3: own native shells, RootsNative bridge, single binary #
- Own native shells:
ios/androidnow build Roots' own thin shells by default: Kotlin + WebView (WebViewAssetLoader, no file/content access, no cleartext) and Swift + WKWebView (aroots://scheme handler, a self-generatedproject.pbxproj). Capacitor stays available with:native (:shell :capacitor). - RootsNative bridge ABI 1:
call(plugin, method, args)andon(event, handler), JSON messages,{code, message}errors, resume/pause/back events, injected only into the app origin's main frame and re-checked natively. - Allow-listed plugins (
:native :plugins), the only ones compiled in:sqlite,storage,clipboard,browser,app, plus app plugins innative/androidandnative/ios.roots/platformon mobile uses them;roots/datauses the nativesqliteplugin. roots build --target android --nativebuilds a debug APK with Gradle (verified: 2.3 MB for the todo app);roots doctorreports Java, Gradle, the Android SDK and Xcode.roots self binary: the whole toolchain as one executable (≈ 48 MB, ≈ 10 ms start, 11 MB with--compress); runtime and shells are embedded and extracted to a hash-checked, read-only cache.roots build --target term --binary: a terminal app as one executable via Bun (warning B156 when Bun is missing).- Reproducible output: generated names count from zero in every compilation unit, so the same sources compile byte for byte identically in every process.
- docs/native.md (shells, ABI, writing plugins, security, signing, what is verified); a single-binary section in docs/build.md.
0.1.2: UI kit, tier 1 (roots/ui) #
- roots/ui (M12): 38 components in typed Roots Lisp (
tsc --strictclean, no eval): Button, IconButton, Input, Textarea, Checkbox, Radio/RadioGroup, Switch, Select, Slider, Label, Field, Form, Dialog, Drawer, Toast, Tooltip, Popover, Menu, Tabs, Accordion, Card, Badge, Tag, Avatar, Divider, ProgressBar, Spinner, Skeleton, List, Table, Tree, ScrollArea, Splitter, Stack, Grid, Center and a 43-icon set (SVG, terminal glyph, ASCII fallback). - Headless machines (tabs, listbox, menu, tree, table, slider, form validation, toasts …) with WAI-ARIA keyboard tables exported as data, shared by every surface.
- Skins per surface family (no platform
ifs): DOM (semantic HTML + ARIA, theme tokens, dark mode, reduced motion) and terminal (borders, glyphs, focus never by colour alone). - Styling hooks:
ptpass-through per part,unstyled, app defaults withcreateUI()/provideUI(). - Runtime:
portal()and an optionalbackend.layer()for overlays (focus trap and restore on both surfaces);runtimeWarnexported for libraries. - Terminal: borderless buttons keep their size when focused; indeterminate checkboxes show
[-]. - Build: the
roots/uispecifier; a runtime module's sibling.cssships inapp.css. - examples/gallery (web and terminal) and docs/ui.md; tests cover the machines in Node, components and frame snapshots in the terminal, a PTY run, and Chromium with zero console errors.
0.1.1: SQLite (roots/data) #
- roots/data (M11): SQLite with one API on every target:
open,run/all/get/exec, transactions with savepoints, a prepared-statement cache, bound parameters only,DataErrorwith D1xx codes. - Live queries:
db.live(sql, params)is a reactive ref that re-runs only when a table it reads is written (tables come from SQLite's EXPLAIN plan plus update hooks); a commit updates once, a rollback never. - Migrations:
migrations/NNN_name.sql, versioned and checksummed, applied bydb.migrate(migrations)orroots db migrate [--check]with identical rules; edited applied migrations are refused. - Drivers per target: node:sqlite / bun:sqlite (term), the official SQLite WASM build in a Worker with OPFS persistence (web, pwa, ext), the RootsNative bridge or
@capacitor-community/sqlite(ios, android). - Build: apps that import roots/data get the SQLite WASM files (SRI-checked
.wasm, PWA precache) and exactly one CSP addition,'wasm-unsafe-eval'; other apps keep the strict policy. - Toolchain:
roots db migrate | types | queryon ansb-alienbinding to libsqlite3;roots db typesgeneratessrc/db-types.tsfor the typedtable<Row, Insert>()helper. - examples/notes and docs/data.md; a driver conformance suite runs on node, bun, Chromium WASM and native/Capacitor mocks.
- Fix:
<script setup>no longer reports C312 forawaitinside functions that declare a TypeScript return type.
0.1.0 (first beta) #
One app source builds for web, PWA, browser extension, iOS, Android and the terminal: roots build --target …. App code is Vue-style SFCs with TypeScript, or Lisp.
- Compiler: Common Lisp → JavaScript and TypeScript (full modern type system), versioned opt-in macro sets, compile-time
target-case. - Parser: JS/TS in Common Lisp; type stripping byte-identical to Node's on 2319/2319 files.
- Templates/SFC: Vue template syntax,
<script setup lang="ts|js|lisp">,defineProps/defineEmits/defineModel, Lispdefcomponent(roots.ui0.1). Compile-time diagnostics for reactivity mistakes and unsafe patterns. - Runtime (in Lisp):
- reactive core with atomic transactions
- components, fine-grained template helpers (no virtual DOM)
- DOM backend, router
- terminal backend with its own flexbox layout, Unicode 17 widths, input and focus
- Build:
roots.lispis data and never evaluated- per-target files
- CSS from a shared utility vocabulary
- strict CSP + SRI
- PWA service worker with update flow, MV3 extension, Capacitor 8 projects
roots new | build | dev | run | doctor
- Dev server: loopback only, allowlist serving, Host/Origin checks, about 45 attack-path regression tests.
- Platform adapters: storage, clipboard, openUrl, notify, info, updates.
- Verified end to end:
- web, pwa and ext in Chromium
- term in a pseudo-terminal
- ios and android structurally (needs macOS + Xcode / the Android SDK to build natively)
- 230+ tests.
- Template diagnostics: no C353 false positive for operators on a ref's member (
!draft.value).
0.0.8 #
- Terminal backend (stream T): own flexbox engine, Unicode 17 widths, cell diff, input, focus, PTY-verified. Compiler: explicit NIL else-branches return null, DECLARE inside handler-case clauses, macrolet, flet/labels as arrow functions (keep this). Build: roots.lisp :theme reaches the terminal. hello-lisp components written with defcomponent.
0.0.7 #
- Compiler: never-reassigned bindings become const (macro expansions and closures are checked), (values) is an expression, async (Promise void) functions return nothing, definterface docstrings anywhere and a clear error when a supertype is written as a generic parameter. Build stream (B) complete: six targets verified end to end.
0.0.6 #
- WIP snapshot of the build (B) and terminal (T) streams, not yet integrated.
0.0.5 #
- Parser (stream P1): JS/TS parser in Common Lisp, position-preserving type stripping byte-identical to Node on 2319/2319 files, scope analysis APIs. Templates (stream P2): HTML template and SFC compiler (<script setup lang=ts|js|lisp>), Lisp components (roots.ui 0.1), compile-time reactivity and security diagnostics. Event modifiers ctrl/shift/alt/meta/exact.
0.0.4 #
- Runtime: components, template helpers, DOM backend and router (stream R); compiler: user definitions shadow built-ins, reserved words renamed, (return nil) → null, no duplicate let for shadowed params; reactivity: stopped child scopes leave their parent, failing jobs no longer strand others, only plain objects/arrays become reactive.
0.0.3 #
- Compiler: NIL compiles to false where the declared type is boolean (variables, fields, assignments, returns); untyped NIL stays null.
0.0.2 #
- Version bumping script (bin/bump); 0.1 road map in MILESTONES.md.
0.0.1 #
- Versioning starts:
roots/VERSION,roots version, this changelog. - Compiler: Common Lisp → JavaScript/TypeScript (M1, M1b), reactive core (M2).
- Versioned macro sets (
define-versioned-macro,use-macros, deprecations); sets are opt-in so library macros never capture your names. - Compile-time targets:
*target*,target-case,when-target,target-name. - CLI command registry (
defcommand), subsystems discovered from.asdfiles,roots testruns every test suite,bin/test-systemruns one. docs/CONTRACT.md: interfaces between parser, template compiler, runtime, backends and build for 0.1.