Skip to the contentSkip to the navigation
RootsGitHubMenu
On this page

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 component can 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, beforeEnter and children; 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 at createRouter, 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 #hash goes to its element, a new page starts at the top. createRouter({ scrollBehavior }) decides otherwise. Positions are kept in history.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.navigationType is "push", "replace" or "pop"; router.afterRender(hook) runs after the new page has rendered.
  • Router: onBeforeRouteLeave and onBeforeRouteUpdate for 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: true always has its values.
  • Router: a function inside a route's props object 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" with checked gives 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: key replaces 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-width for 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-success and --roots-on-info are 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 compare gets 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 fill its flex parent (fill works 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.onChange follow 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.reason says 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 reset deletes the local database and applies the migrations again; D132 now says how to start again while developing.
  • roots db types no longer marks a view's columns nullable when they come from NOT NULL columns through inner joins, and types an INTEGER PRIMARY KEY DESC column (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, query and types work 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") from roots/platform, with theme.current and theme.resolved as 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 under data-theme="light"; an app's own :root { --roots-… } still overrides them.
  • roots/platform has screen: columns, rows, width and height as refs that follow resizes, and screen.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 :primary for 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 :theme text 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: and blob: URLs under the generated Content-Security-Policy, in the built page and under roots dev and roots 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-center and items-end now place Links, Buttons, Checkboxes and Inputs.
  • In the terminal, a flex-1 item 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 :dark on a dark screen.
  • The terminal no longer warns about classes of the app's own CSS, dom: classes, or the router's r-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 dev runs the link check of roots build: a misspelt named import is B134 in the browser overlay and the terminal instead of a white page; roots dev --target term checks the same before it starts the app.
  • roots dev shows the build's warnings in the page, in a panel that can be closed.
  • roots dev shows run-time errors in the page: errors nobody handled, rejected promises, and errors reported with the roots:error event, 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.css swaps the stylesheet without reloading the page.
  • The rebuild line of roots dev reports 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/roots stops with roots: 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/roots no longer prints SBCL's compiler notes after a toolchain change; ROOTS_DEBUG=1 shows 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, the theme and screen APIs, toast key and 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 preview and serve-static send img-src 'self' data: blob: in the HTML CSP header (it had no img-src, so default-src 'self' applied). A project :img-src list 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 its flex-1 items; it used to count them as 0.
  • roots build --target web prints or 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: 0 or createToaster({ 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 own label has no aria-label. An uncontrolled Select shows its value.
  • Kit controls in a Field get aria-labelledby unless they have their own label; the Field label has an id; RadioGroup and Slider fields render no for=; the Field root carries data-reveal.
  • Input, Textarea and Select focus: a solid 2px outline replaces the 30 % ring; an invalid Input shows the error border.
  • .r-form is a flex column, so its 1rem gap is visible.
  • Grid always has one root box; attributes land on it instead of warning R211.
  • Table compare is 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-reveal ancestor.
  • Additions: ToastApi.dismissNewest(), Toaster.setMax(), <Toaster :max>, FieldContext.labelId(), setControl(node, labelable?), a third argument to controlAttrs, ScrollArea fill, Grid minItemWidth, Pagination, NumberInput.
  • A route component that 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 in defineComponent.
  • createRouter throws 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.state also carries scroll; the first entry is stamped {position: 0}; each push does one extra replaceState.
  • A history entry not made by the router gets position + 1 and navigationType: "push".
  • A Back/Forward whose guard or loader throws puts the URL back.
  • Function values in a props object are no longer called as getters.
  • Route locations have matchedRoutes and navigationType.
  • R404 message: "A navigation was redirected more than 10 times".
  • A child route's meta is a merged copy of parent and own meta.
  • In the browser a second tab shares the first tab's database (persistent true, no D151). In-memory fallback remains without OPFS, Web Locks or BroadcastChannel, or next to a tab of an older build.
  • db.onChange and 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|reset work without the entry component.
  • EXPLAIN runs with NULL for every placeholder (exact invalidation for parameterized live queries on the iOS plugin).
  • Driver interface additions: reason, notify, subscribe, DriverOptions.setup; types LiveSource, 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-warning reads --roots-warning-text; base button and input borders 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.js and one blocking <script> appear only in apps that import theme.
  • 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-center or items-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-active is bold.
  • The terminal bootstrap imports the backend namespace and calls setDarkTheme?. / setAppClasses?.; the terminal platform adapter imports runtime/term/display.lisp.
  • R504 for an invalid theme.set value.
  • roots dev fails a build that does not link (B134, B132, B135 …); --target term does not start Node on a link error.
  • /__roots/events: ok data is {"build": n, "warnings": [...]}; new css event.
  • 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/roots exits 1 when a present subsystem fails to load; compiler notes are hidden without ROOTS_DEBUG.
  • New command roots preview.
  • Router: the scroll position of the entry on screen is written to history.state 150 ms after the window stopped scrolling (one more replaceState per scroll gesture) and on visibilitychange → hidden, not only on pagehide.
  • 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 #42b883 the 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-accent no longer reaches these (set --roots-on-primary and its siblings); --r-on-accent remains 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 under on the parts dismiss and list / panel).
  • Kit: a toast replaced through key while 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 class r-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 #111827 reaches 4.5:1.
  • Theme: the theme API reads, writes and listens only from its first use; an app that never uses it keeps its own data-theme under roots dev too.
  • Terminal: while a page scrolls, the app is laid out one column narrower (79 of 80); screen.columns stays the terminal's width.
  • Data: roots db types types an INTEGER PRIMARY KEY DESC column as nullable, in the table's Row type and in views (it was number), 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 in v-for rows are shared (rowHandler, onRow); a class that asks a createSelector result 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% of esbuild --minify on 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. on is split from onWith (event modifiers), and bundles for DOM targets define CLONE_BACKEND.
  • roots build --error-codes (opt-in; :error-codes t in 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.src and form.action answer the unresolved attribute, and an <img> with a static src starts loading at insertion. Templates with a custom element, a <video> or an <audio> are not moved.
    • The v-for item is the list's record, not a shallowRef: .value is tracked and read-only, isRef(item) is false. Assigning a v-for alias or a slot prop in a template (n++, row = x) is compile error C254; row.done = true works 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-for rows 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 :selector in the template compiler; v-model on 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 defcomponent templates emit $.onWith for an event with modifiers.
    • R307 and R308 are raised through the DOM backend's warn (same text and console output, still not through the warnHandler).
    • 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, firstChild and isElement needs --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).
  • 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 .lisp file, or a .vue with <script lang="lisp">, was compiled like app code, and compiling Lisp runs its macros: any package could run Common Lisp as the developer on every roots build and roots dev. It is now B129, with no opt-in; while a package file is compiled the compiler also refuses (lisp …), defmacro and macrolet. Importing into node_modules by 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 var in 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 an isAllowed check 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 a let / const in 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 build wrote 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/ and native/<platform>/ are never read through a link (B163), and more key material is kept out of public/ (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 symlinked node_modules/.roots no longer makes roots install delete 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 .npmrc files 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 (or roots install --verify-age) close it for projects that set them.
  • SQLite WASM is pinned by hash. roots/data shipped index.mjs and sqlite3.wasm from whatever node_modules was 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 :data key.
  • 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, srcDoc or HREF from 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. :csp in roots.lisp adds origins to the policy (never to script-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; Host needs the port; internal errors during a rebuild no longer end roots dev.
  • roots/data: roots db types and migrate --check could write arbitrary files through a migration (ATTACH); toolchain connections cannot attach now, and a migration with BEGIN / COMMIT / ROLLBACK is refused before anything runs (D136). At run time ATTACH needs { attach: true } (D117), trusted_schema is 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, and roots db query cannot 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 with BRIDGE_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 .js syntax error one line with file and position, anything unexpected B191 (ROOTS_DEBUG=1 for the backtrace). import "pkg" keeps the module in a bundle whatever the package's sideEffects says.
  • 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 …/tests systems (471 pass here; e2e-extension-popup cannot 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/reactivity 3.5.44: 1.69× and 1.75× in the two load orders (1.44–1.45 at 0.3.0); create 1to1000 is level with Vue (0.96× / 1.00×), create 1to1 ahead.
  • What changed: the graph functions test links and nodes with !== null instead of truthiness (V8's optimized code for the hot functions is 17–44% smaller); EffectBase assigns its fields in the constructor, so creating an effect no longer goes through a generic Construct call; an effect whose function declares onCleanup keeps 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-class takes (:constructor-fields t) (fields are assigned at the start of the constructor in declaration order; no class-field syntax; TypeScript output declares them with declare); (some? x) and (none? x) compile to x !== null / x === null. No existing compiled output changed (120 sources byte-identical).
  • Behaviour at the edges: a ReactiveEffect is 96 B (was 88; one declaring onCleanup 144 B, was 226); its scheduler is fixed at construction (null counts as none); new exports for list bindings: trackDep, triggerDep, selectorBind, selectorUnbind. onCleanup and 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 behind run-reactivity.mjs --libs; a hand-written app that clones rows from a <template> (vanillatpl, a lower floor than vanilla on creation); two cases for effects that declare onCleanup; --presweep, --browser firefox; run-startup.mjs accepts 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 build bundles and minifies by default. The bundler, tree shaker and minifier are written in Lisp (src/bundle/, system roots-bundle, docs/bundling.md): one ES module per entry, tree-shaken at the granularity of top-level statements (sideEffects of packages, /* @__PURE__ */ and /* @__NO_SIDE_EFFECTS__ */ annotations, a pure-expression grammar), defines and constant folding (process.env.NODE_ENV, the runtime's DEV), 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-bundle keeps one file per module, --no-minify a readable bundle; :bundle / :minify in roots.lisp, also per target. roots dev and roots run never 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 --minify over 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-bundle output (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 to chunks/<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.url outside new URL("./…", import.meta.url), or such a URL leaving the output directory), B134 (warning for ns.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 and defcomponent are marked pure, so an app that imports one kit component does not carry the others.
  • DEV is false in roots build (bundled or not): prop validation (R202, R203, validator calls), the R215 scan of pure insertions and the R224 check run in roots dev and roots run only. Prop validation and the R215 scan ran in every build before; the R224 check is new and development-only.
  • process.env.NODE_ENV is a build-time literal in every build, not only in the bundle (0.2.1 never replaced it): "production" in roots build, bundled or not, "development" in roots dev and roots run. It is replaced in npm packages on every target and in app code on DOM targets; app code on term keeps the real process.env, and it is never replaced in the runtime.
  • roots run and roots dev --target term write a development build (unbundled, DEV true, NODE_ENV "development") into dist/term, where 0.2.1 left the same output as roots build --target term. It replaces a production build in that directory: run roots build --target term again before shipping.
  • Unbundled output (--no-bundle, roots dev; DOM targets): a modulepreload link with SRI per statically imported module and the compile hint //# allFunctionsCalledOnLoad on the modules the toolchain compiled (runtime, .vue, .ts, .lisp; under 128 KB; not the bootstrap, plain .js files 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, Image and Spacer go 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 Box 12%, a bare div 6%), 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×) and create 1to1000 (0.66–0.67×; the fn.length read that keeps onCleanup'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/fill 0.82×, if/updateAll 0.85×, attr/updateAll 0.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-for item getCurrentScope() returns the item's record (a Scope without run/stop); props are validated in development builds only and a default factory 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 first null/undefined/removing false is not written (bindAttr(node, name, getter, over?) for hand-written render code); components without attrs share one empty attrs object; props/attrs views throw R204 on defineProperty, setPrototypeOf and preventExtensions too; the shared onCleanup throws R106 inside untrack, a computed's getter and a watch getter; an EffectScope does 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, model and Link never 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 codes R224, R225. New optional backend methods: isElement, collect, setTextContent, listen, adopt. Two new requirements for custom backends: a node fresh from createElement has every unset prop absent (type and aria-hidden excepted), and host nodes get an own property keyed by Symbol("roots.nodeState") (a non-extensible node gets a WeakMap entry instead).
  • Compiler: the variables a loop generates are numbered from the second loop of a compilation unit on (result2, end2; repeat counts 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; no MSG_NOSIGNAL on 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 --out directory 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)), and roots add alias@npm:real checks real, 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/tests alone 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) and data-runtime-conformance-bun (6 of its 20 cases: SQLITE_BUSY on commit under the installed Bun's bun: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.md moved 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's node_modules, not the parent directory's.
  • Development dependencies are declared in package.json and 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/notes installs 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; .gitignore for node_modules/ and dist/.

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 RootsNative bridge (ABI 1) and allow-listed plugins; Capacitor stays selectable. roots self binary writes 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 by sbcl --script docs/presentation/build.lisp from the real sources, tests and benchmark tables; presentation-builds checks 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.mjs empties 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 onCleanup registrar lives in a WeakMap); building a graph skips the duplicate-link search for deps without subscribers; alternating reads (a, b, a) are found in link without a call; connect/disconnect are lighter. 1.41× @vue/reactivity 3.5 (geometric mean of 30 cases over three whole-suite runs: 1.39, 1.42, 1.41); unstable 1.1× (was below parity), create 1000to1 1.2×, create 1to1 and create+dispose at parity. Rendering unchanged: script 2.06×, totals 1.17×.
  • Known gap: create 1to1000 is 0.75–0.83×: effect() reads fn.length (~10 ns) so that a declared onCleanup parameter can be called after the run, as R106 promises. The semantics are kept; Vue's onEffectCleanup model 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 with currentTarget set and stopPropagation() 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 / defparameter emit var (no temporal-dead-zone check on every access); inside functions they stay let.
  • 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/reactivity 3.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 :plugins lists Capacitor npm plugins while the target uses the Roots shell, which ignores them; the hint points to :native :plugins or (:shell :capacitor).
  • examples/notes uses the Roots shell's native sqlite plugin 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 :reason in roots.lisp :packages; --allow-young is recorded in roots.lock; PM131 names the newest eligible version when nothing fits.
  • No dependency code runs at install: roots approve-builds pins 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.lock is deterministic text, re-checked on every install (registry hosts, integrity, name/version/dependencies against each tarball's package.json); --frozen is 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 verify re-hashes everything.
  • Build: apps import declared npm packages (exports → module → main, emitted under npm/<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 --ci workflow 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/reactivity 3.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 $.template spec cloned with one cloneNode (new helpers template, clone, bindText, insert; optional backend members cloneNode, 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 leading this parameter types this in TypeScript, a new for-in form.
  • Benchmarks: runtime/bench/run-reactivity.mjs and run-render.mjs (Chromium, vs Vue 3.5 and vanilla), described in docs/performance.md.
  • Behaviour: new error R106 (a shared onCleanup called 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/android now build Roots' own thin shells by default: Kotlin + WebView (WebViewAssetLoader, no file/content access, no cleartext) and Swift + WKWebView (a roots:// scheme handler, a self-generated project.pbxproj). Capacitor stays available with :native (:shell :capacitor).
  • RootsNative bridge ABI 1: call(plugin, method, args) and on(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 in native/android and native/ios. roots/platform on mobile uses them; roots/data uses the native sqlite plugin.
  • roots build --target android --native builds a debug APK with Gradle (verified: 2.3 MB for the todo app); roots doctor reports 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 --strict clean, 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: pt pass-through per part, unstyled, app defaults with createUI() / provideUI().
  • Runtime: portal() and an optional backend.layer() for overlays (focus trap and restore on both surfaces); runtimeWarn exported for libraries.
  • Terminal: borderless buttons keep their size when focused; indeterminate checkboxes show [-].
  • Build: the roots/ui specifier; a runtime module's sibling .css ships in app.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, DataError with 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 by db.migrate(migrations) or roots 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 | query on an sb-alien binding to libsqlite3; roots db types generates src/db-types.ts for the typed table<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 for await inside 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, Lisp defcomponent (roots.ui 0.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.lisp is 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 .asd files, roots test runs every test suite, bin/test-system runs one.
  • docs/CONTRACT.md: interfaces between parser, template compiler, runtime, backends and build for 0.1.