squint-cljs / squint

Light-weight ClojureScript dialect
https://squint-cljs.github.io/squint
587 stars 35 forks source link
clojure clojurescript javascript

Squint

Squint is a light-weight dialect of ClojureScript with a compiler and standard library.

Squint is not intended as a full replacement for ClojureScript but as a tool to target JS when you need something more light-weight in terms of interop and bundle size. The most significant different with CLJS is that squint uses only built-in JS data structures. Squint's output is designed to work well with ES modules.

If you want to use squint, but with the normal ClojureScript standard library and data structures, check out cherry.

:warning: This project is a work in progress and may still undergo breaking changes.

Quickstart

Although it's early days, you're welcome to try out squint and submit issues.

$ mkdir squint-test && cd squint-test
$ npm init -y
$ npm install squint-cljs@latest

Create a .cljs file, e.g. example.cljs:

(ns example
  (:require ["fs" :as fs]
            ["url" :refer [fileURLToPath]]))

(println (fs/existsSync (fileURLToPath js/import.meta.url)))

(defn foo [{:keys [a b c]}]
  (+ a b c))

(println (foo {:a 1 :b 2 :c 3}))

Then compile and run (run does both):

$ npx squint run example.cljs
true
6

Run npx squint --help to see all command line options.

Why Squint

Squint lets you write CLJS syntax but emits small JS output, while still having parts of the CLJS standard library available (ported to mutable data structures, so with caveats). This may work especially well for projects e.g. that you'd like to deploy on CloudFlare workers, node scripts, Github actions, etc. that need the extra performance, startup time and/or small bundle size.

Talk

ClojureScript re-imagined at Dutch Clojure Days 2022

(slides)

Differences with ClojureScript

If you are looking for closer ClojureScript semantics, take a look at Cherry 🍒.

Articles

Projects using squint

Advent of Code

Solve Advent of Code puzzles with squint here.

Seqs

Squint does not implement Clojure seqs. Instead it uses the JavaScript iteration protocols to work with collections. What this means in practice is the following:

Most collections are iterable already, so seq and iterable will simply return them; an exception are objects created via {:a 1}, where seq and iterable will return the result of Object.entries.

first, rest, map, reduce et al. call iterable on the collection before processing, and functions that typically return seqs instead return an array of the results.

Memory usage

With respect to memory usage:

(js/global.gc)

(println (js/process.memoryUsage))

(defn doit []
  (let [x [(-> (new Array 10000000)
               (.fill 0)) :foo :bar]
        ;; Big array `x` is still being held on to by `y`:
        y (rest x)]
    (println (js/process.memoryUsage))
    (vec y)))

(println (doit))

(js/global.gc)
;; Note that big array is garbage collected now:
(println (js/process.memoryUsage))

Run the above program with node --expose-gc ./node_cli mem.cljs

JSX

You can produce JSX syntax using the #jsx tag:

#jsx [:div "Hello"]

produces:

<div>Hello</div>

and outputs the .jsx extension automatically.

You can use Clojure expressions within #jsx expressions:

(let [x 1] #jsx [:div (inc x)])

Note that when using a Clojure expression, you escape the JSX context so when you need to return more JSX, use the #jsx once again:

(let [x 1]
  #jsx [:div
         (if (odd? x)
           #jsx [:span "Odd"]
           #jsx [:span "Even"])])

To pass props, you can use :&:

(let [props {:a 1}]
  #jsx [App {:& props}])

See an example of an application using JSX here (source).

Play with JSX non the playground

HTML

Similar to JSX, squint allows you to produce HTML as strings using hiccup notation:

(def my-html #html [:div "Hello"])

will set the my-html variable to a string equal to <div>Hello</div>.

Using metadata you can modify the tag function, e.g. to use this together with lit-html:

(ns my-app
  (:require ["lit" :as lit]))

#html ^lit/html [:div "Hello"]

This will produce:

lit/html`<div>Hello</div>`

See this playground example for a full example.

Async/await

Squint supports async/await:

(defn ^:async foo [] (js/Promise.resolve 10))

(def x (js-await (foo)))

(println x) ;;=> 10

Anonymous functions must have ^:async on the fn symbol or the function's name:

(^:async fn [] (js-await {}) 3)

Generator functions

Generator functions must be marked with ^:gen:

(defn ^:gen foo []
  (js-yield 1)
  (js-yield* [2 3])
  (let [x (inc 3)]
    (yield x)))

(vec (foo)) ;;=> [1 2 3 4]

Anonymous functions must have ^:gen on the argument vector:

(^:gen fn [] (js-yield 1) (js-yield 2))

See the playground for an example.

Arrow functions

If for some reason you need to emit arrow functions () => ... rather than function, you can use :=> metadata on the function expression, fn symbol or argument vector:

(fn ^:=> [] 1)

Defclass

See doc/defclass.md.

JS API

The JavaScript API exposes the compileString function:

import { compileString } from 'squint-cljs';

const f = eval(compileString("(fn [] 1)"
                             , {"context": "expr",
                                "elide-imports": true}
                            ));

console.log(f()); // prints 1

REPL

Squint has a console repl which can be started with squint repl.

nREPL

A (currently immature!) nREPL implementation can be used on Node.js with:

squint nrepl-server :port 1888

Please try it out and file issues so it can be improved.

Emacs

You can use this together with inf-clojure in emacs as follows:

In a .cljs buffer, type M-x inf-clojure. Then enter the startup command npx squint repl (or bunx --bun repl) and select the clojure or babashka type REPL. REPL away!

Truthiness

Squint respect CLJS truth semantics: only null, undefined and false are non-truthy, 0 and "" are truthy.

Macros

To load macros, add a squint.edn file in the root of your project with {:paths ["src-squint"]} that describes where to find your macro files. Macros are written in .cljs or .cljc files and are executed using SCI.

The following searches for a foo/macros.cljc file in the :paths described in squint.edn.

(ns foo (:require-macros [foo.macros :refer [my-macro]]))

(my-macro 1 2 3)

squint.edn

In squint.edn you can describe the following options:

See examples/vite-react for an example project which uses a squint.edn.

Watch

Run npx squint watch to watch the source directories described in squint.edn and they will be (re-)compiled whenever they change. See examples/vite-react for an example project which uses this.

Svelte

A svelte pre-processor for squint can be found here.

Vite

See examples/vite-react.

React Native (Expo)

See examples/expo-react-native.

Compile on a server, use in a browser

See examples/babashka/index.clj.

Playground

License

Squint is licensed under the EPL. See epl-v10.html in the root directory for more information.