Choq (Cherry on QuickJS) is a ~5MB binary chock full of scripting goodies. It runs the cherry compiler inside an embedded QuickJS engine via rquickjs.
Status
Experimental.
When to use choq
Choq trades peak performance for size and memory. QuickJS has no JIT, so hot code runs slower than on Node.js, Bun or Deno, but the binary is small, startup is fast and memory use stays low: in local measurements a Hono app serves around 30k requests/s and uses less memory than the same app on Node.js or Bun.
Use it as a lighter alternative to Node.js for scripts and small servers, in the same spirit as babashka next to the JVM: personal projects on a VPS, small boards like a Raspberry Pi, or anywhere a full JS runtime is too heavy.
Install
The install script puts choq in /usr/local/bin. Use sudo when
that directory is not writable, or pick another directory with
--dir:
curl -sL https://raw.githubusercontent.com/squint-cljs/choq/main/install | sudo bash # into a specific directory, no sudo: curl -sL https://raw.githubusercontent.com/squint-cljs/choq/main/install | bash -s -- --dir ~/bin
Works on macOS, Linux and Windows (Git Bash). Binaries are also on the dev release.
Usage
$ choq Choq REPL, Ctrl-D to exit user=> (defn foo [x] (inc x)) #object[foo] user=> (foo 41) 42 user=> (require '[clojure.string :as str]) nil user=> (str/join "-" (map inc [1 2 3])) "2-3-4"
-e evaluates one expression and prints the non-nil result:
$ choq -e '(map inc [1 2 3])'
(2 3 4)Pass a file argument to run a script:
--nrepl starts an nREPL server and writes .nrepl-port for editor
connections:
$ choq --nrepl # port 1339
$ choq --nrepl 7888API
Embedded cherry namespaces: clojure.string, clojure.set,
clojure.walk, cljs.pprint, clojure.test.
Node modules from LLRT, also
available under their node: names: fs, fs/promises, path,
buffer, timers, tty, crypto, net, os, process, url, util, zlib.
API.md lists the
functions that each module supports. stream, events and
string_decoder are vendored js implementations based on
readable-stream.
Globals: Buffer, console, crypto, process, fetch, Headers,
Request, Response, FormData, setTimeout, clearTimeout,
setInterval, clearInterval, setImmediate.
*e holds the last repl exception: (.-stack *e) prints where it
happened.
await works on all promises:
(require '["node:fs/promises" :as fs]) (await (fs/readFile "README.md" "utf8")) (-> (await (js/fetch "https://api.github.com/zen")) (.text) await)
HTTP server
choq.http/serve starts an HTTP server on 127.0.0.1. The handler
takes a fetch API Request and returns a Response, or a promise of
one:
(require '[choq.http :refer [serve]]) (serve (fn [req] (js/Response. "hello")) {:port 3000})
A Hono app plugs in directly:
(require '["https://esm.sh/hono" :refer [Hono]] '[choq.http :refer [serve]]) (def app (Hono.)) (.get app "/" (fn [c] (.text c "hello from hono"))) (serve (.-fetch app) {:port 3000})
A running server keeps the process alive. JS modules import the same
API from choq:http.
URL imports
https:// specifiers load like in Deno. Downloads are cached in
~/.cache/choq:
(require '["https://esm.sh/lodash-es@4.17.21" :as l]) (l/camelCase "foo bar")
Pin the version in the URL. An unpinned URL stays at the version that the first download returned.
Downloads send a Node user agent, so esm.sh serves node builds instead
of browser builds. Node builtin imports in remote modules (node:fs
and friends, or esm.sh's /node/*.mjs shims) resolve to the native
modules above, so npm libraries that use them work:
(require '["https://esm.sh/@babashka/fs" :as bfs]) (bfs/slurp "README.md")
Local namespaces
Bare namespace requires resolve to .cljs and .cljc files on .,
src and test:
(require '[my.util :as util]) ;; loads src/my/util.cljs
Clojure dependencies
choq.deps/add-deps resolves Clojure libraries from Clojars and Maven
Central. Namespaces from installed libraries load with plain require
afterwards:
(require '[choq.deps :as deps]) (deps/add-deps '{:deps {dev.weavejester/medley {:mvn/version "1.8.0"}}}) (require '[medley.core :as m]) (m/index-by :id [{:id 1} {:id 2}])
A mvn: specifier resolves the library and requires the namespace in
one step. This runs babashka.fs, the real one from clojars:
(require '["mvn:babashka/fs@0.5.34/babashka.fs" :as fs]) (fs/exists? "README.md")
Build from source
pnpm install cargo build --release
Run the tests:
target/debug/choq test/run_tests.cljs
Implementation
The compiler and the standard library come from the cherry-cljs npm package, pinned in package.json. build.rs compiles these modules to QuickJS bytecode and embeds them in the binary. The REPL compiles each input inside the engine and evaluates it in the same context. The node modules are LLRT's Rust implementations, driven by a tokio event loop. Startup takes about 20ms.