diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index fbe1d45a..af858c4b 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -1,4 +1,4 @@ -name: Playground +name: Site on: push: branches: @@ -34,18 +34,24 @@ jobs: cli: latest bb: latest - - name: Install and Build 🔧 + - name: Build playground 🔧 run: | bb build cd playground && bb build + - name: Assemble site + run: | + mkdir -p _site + cp site/*.html site/*.css site/*.svg site/*.cljs site/*.mjs _site/ + cp -r playground/public/dist _site/squint + - name: Setup Pages uses: actions/configure-pages@v3 - name: Upload artifact uses: actions/upload-pages-artifact@v3.0.1 with: - path: 'playground/public/dist' + path: '_site' - name: Deploy to GitHub Pages id: deployment diff --git a/site/api.html b/site/api.html new file mode 100644 index 00000000..7ba22356 --- /dev/null +++ b/site/api.html @@ -0,0 +1,409 @@ + + + + + + + + + API Listing — Squint + + + + +
+

API Listing

+ +

This page documents Squint's special forms, compiler API, and + configuration options.

+ +
+ Contents + +
+ + + +

Special forms

+ +

These are built into the compiler and cannot be overridden.

+ +

if

+
(if test then else)
+

Evaluates test. If truthy, evaluates and returns + then, otherwise else. Both branches are + optional (missing else returns nil). + Note: Squint follows ClojureScript truthiness — only nil, + undefined, and false are falsy. + 0 and "" are truthy.

+ +

let

+
(let [name value ...] body)
+

Creates local bindings. Bindings are sequential — later bindings + can reference earlier ones. Supports destructuring. Compiles to + const declarations. Destructuring uses + squint_core.get() rather than JS native destructuring.

+ +

do

+
(do expr1 expr2 ...)
+

Evaluates expressions in order, returns the last. Used to group + multiple expressions where only one is expected.

+ +

loop / recur

+
(loop [i 0]
+  (when (< i 10)
+    (println i)
+    (recur (inc i))))
+

Compiles to a while(true) loop with variable reassignment. + recur can only appear in tail position. Also works with + defn for self-recursion.

+ +

when

+
(when test body...)
+

If test is truthy, evaluates all body expressions. + Returns nil if test is falsy. No else branch.

+ +

cond

+
(cond
+  test1 expr1
+  test2 expr2
+  :else default)
+

Multi-branch conditional. Tests are evaluated in order; the first + truthy test's expression is returned.

+ +

case

+
(case expr
+  value1 result1
+  value2 result2
+  default-result)
+

Constant-time dispatch. Compiles to a JavaScript switch + statement. The last form without a matching value is the default.

+ +

try / catch / finally

+
(try
+  (risky-operation)
+  (catch e
+    (println "Error:" (.-message e)))
+  (finally
+    (cleanup)))
+ +

throw

+
(throw (js/Error. "Something went wrong"))
+ +

quote / '

+
'(1 2 3)  ; returns the list as data
+ + + +

Definitions

+ +

def

+
(def name value)
+

Defines a module-level binding. Exported from the module by default. + Use ^:private metadata to prevent export.

+ +

defn

+
(defn name [params...] body)
+(defn name "docstring" [params...] body)
+(defn name
+  ([x] ...)
+  ([x y] ...))
+

Defines a named function. Supports docstrings, multiple arities, + destructuring, variadic args (&), and metadata + (^:async, ^:private, ^:gen).

+ +

fn

+
(fn [x] (+ x 1))
+(fn my-fn [x] (+ x 1))
+

Anonymous function. Compiles to a function expression. Use + ^:=> metadata for arrow function output.

+ +

defmacro

+
(defmacro name [params...] body)
+

Defines a compile-time macro. See the Macro + Guide for details.

+ +

defclass

+
(defclass MyClass
+  (extends ParentClass)
+
+  ;; Instance fields
+  (field -x)
+  (field -y :default)
+
+  ;; Static field
+  (^:static field -count 0)
+
+  ;; Constructor
+  (constructor [this x y]
+    (super x)
+    (set! -x x)
+    (set! -y y))
+
+  ;; Instance methods
+  Object
+  (toString [this]
+    (str "MyClass(" -x ")"))
+
+  ;; Static method
+  (^:static create [_ x]
+    (js/MyClass. x nil))
+
+  ;; Async method
+  (^:async fetch [this url]
+    (js-await (js/fetch url))))
+

Defines a JavaScript class. Supports:

+ +

See the defclass + documentation for full details.

+ + + +

JavaScript interop

+ +

js/ — global access

+
(js/console.log "hello")
+(def now (js/Date.now))
+(def obj (js/URL. "https://example.com"))
+

Access global JavaScript objects. Trailing . means + constructor call (new).

+ +

. — method call

+
(.toUpperCase "hello")    ; "HELLO"
+(.slice arr 1 3)           ; subarray
+ +

.- — property access

+
(.-length "hello")        ; 5
+(.-protocol location)    ; "https:"
+ +

set! — property assignment

+
(set! (.-title js/document) "My App")
+ +

#js — JS literal

+
#js {:a 1 :b 2}   ; JS object literal
+#js [1 2 3]       ; JS array literal
+ + + +

Async and generators

+ +

^:async / js-await

+
(defn ^:async fetch-data [url]
+  (let [res (js-await (js/fetch url))]
+    (js-await (.json res))))
+

Compiles to async function / await. + The ^:async metadata can be applied to defn + or fn.

+ +

^:gen / js-yield

+
(defn ^:gen range-gen [start end]
+  (loop [i start]
+    (when (< i end)
+      (js-yield i)
+      (recur (inc i)))))
+

Compiles to a generator function (function*) with + yield. Use js-yield* to delegate to another + iterable (like yield* in JS):

+ +
(defn ^:gen all-items []
+  (js-yield 0)
+  (js-yield* [1 2 3])
+  (js-yield 4))
+ +

^:=> — arrow functions

+
(fn ^:=> [x] (+ x 1))
+

Emits a JavaScript arrow function instead of a regular function + expression. Useful for callbacks where you need lexical this.

+ + + +

JSX and HTML

+ +

#jsx

+
#jsx [:div {:className "app"}
+  [:h1 "Hello"]
+  [:p "World"]]
+

Converts hiccup-style vectors to JSX. Works with React and any + JSX-consuming library.

+ +

#html

+
#html [:div
+  [:h1 "Hello"]
+  [:p "World"]]
+
+;; Hiccup shorthand for id and class
+#html [:div.container#main
+  [:p.intro "Welcome"]]
+
+;; Raw unescaped output via :$
+#html [:$ "<!DOCTYPE html>"]
+

Generates HTML strings from hiccup syntax. Content is escaped by + default. Use [:$ ...] for raw unescaped output. Supports + hiccup shorthand for id (#) and class (.) + attributes. Useful for server-side rendering or static site + generation.

+ + + +

Standard library highlights

+ +

Squint re-implements a subset of Clojure's standard library for + JavaScript data structures. Here are the most commonly used + functions:

+ +

Collections

+
map filter reduce remove keep
+first rest next last butlast
+nth count empty? seq
+conj cons into concat
+take drop take-while drop-while
+partition partition-by group-by
+sort sort-by reverse
+distinct dedupe frequencies
+interleave interpose flatten
+some every? not-every? not-any?
+ +

Maps (objects)

+
get get-in assoc assoc-in dissoc
+update update-in merge
+select-keys rename-keys
+keys vals
+contains? find
+ +

Strings

+
str subs name namespace
+clojure.string/join
+clojure.string/split
+clojure.string/replace
+clojure.string/trim
+clojure.string/upper-case
+clojure.string/lower-case
+clojure.string/starts-with?
+clojure.string/ends-with?
+clojure.string/includes?
+clojure.string/blank?
+ +

Math and comparison

+
+ - * / mod rem quot
+inc dec max min abs
+= not= < > <= >=
+zero? pos? neg? even? odd?
+number? string? fn? nil? boolean?
+ +

State

+
;; Atoms
+atom deref swap! reset! add-watch remove-watch
+compare-and-swap! swap-vals! reset-vals!
+
+;; Volatiles (faster than atoms, no watches)
+volatile! vreset! vswap!
+ +

Functions

+
apply comp partial complement juxt
+identity constantly fnil
+memoize
+ +

For the complete list, see the + Squint README + on GitHub.

+ + + +

Compiler API

+ +

You can use Squint's compiler programmatically from JavaScript:

+ +
import { compileString } from "squint-cljs";
+
+const result = compileString('(defn greet [x] (str "Hi " x))');
+console.log(result);
+// function greet(x) { return "Hi " + x; }
+ +

compileString accepts options:

+ + + + + +

Configuration (squint.edn)

+ +

Project-level configuration is stored in squint.edn at + the project root:

+ +
{:paths ["src"]
+ :output-dir "out"
+ :extension "mjs"
+ :copy-resources [".json" ".svg"]
+ :import-maps {"react" "https://esm.sh/react"}}
+ +

Key options:

+ + + + + +

CLI commands

+ +
$ npx squint run file.cljs              # Compile and run
+$ npx squint compile file.cljs          # Compile to .mjs
+$ npx squint watch                      # Watch and recompile on changes
+$ npx squint repl                       # Start interactive REPL
+$ npx squint nrepl-server :port 3333    # Start nREPL server
+ +

All commands respect the squint.edn configuration file + if present.

+ +
+
+ + + + diff --git a/site/bundlers.html b/site/bundlers.html new file mode 100644 index 00000000..6fbe97d5 --- /dev/null +++ b/site/bundlers.html @@ -0,0 +1,191 @@ + + + + + + + + + Bundler Integration — Squint + + + + +
+

Bundler Integration

+ +

Squint compiles to standard ES modules, so it works with any + JavaScript bundler. This guide covers the most common setups.

+ +
+ Contents + +
+ +

General workflow

+ +

The typical development flow is:

+ +
    +
  1. Write .cljs files in src/
  2. +
  3. Run npx squint watch to compile to .mjs + in out/
  4. +
  5. Point your bundler at the compiled .mjs files
  6. +
  7. The bundler handles bundling, minification, and dev server
  8. +
+ +

Squint's output is plain ES modules — no special loader or plugin + is needed. The bundler sees standard JavaScript.

+ +

Namespace to path mapping: Squint converts namespace names + to file paths — hyphens become underscores and dots become directory + separators. For example, namespace my-app.core compiles + to my_app/core.mjs.

+ +

Configure source and output directories in squint.edn:

+ +
{:paths ["src"]
+ :output-dir "out"
+ :extension "mjs"
+ :copy-resources [".css" ".html" ".svg"]}
+ +

Vite

+ +

Vite is the recommended bundler for Squint web projects. It's fast, + requires minimal configuration, and handles ES modules natively.

+ +
$ npm install vite --save-dev
+ +

Create a minimal vite.config.js:

+ +
export default {
+  root: "out",       // point at Squint's output directory
+  build: {
+    outDir: "../dist"  // final build output
+  }
+};
+ +

Create out/index.html that loads your entry point:

+ +
<script type="module" src="./my_app/core.mjs"></script>
+ +

Run both watchers in parallel:

+ +
# Terminal 1: compile Squint
+$ npx squint watch
+
+# Terminal 2: Vite dev server
+$ npx vite out
+ +

Vite's HMR (Hot Module Replacement) will pick up changes as + Squint recompiles your files.

+ +

esbuild

+ +

esbuild is the fastest bundler and ideal for production builds:

+ +
$ npm install esbuild --save-dev
+ +
# Bundle for the browser
+$ npx esbuild out/my_app/core.mjs \
+    --bundle --minify --outfile=dist/app.js
+
+# Bundle for Node.js
+$ npx esbuild out/my_app/core.mjs \
+    --bundle --platform=node --outfile=dist/app.cjs
+ +

esbuild produces extremely small bundles. Combined with Squint's + already-small output, this gives you the best possible bundle sizes.

+ +

Node.js (no bundler)

+ +

For scripts, CLI tools, and servers, no bundler is needed. Run + the compiled output directly:

+ +
# Compile and run directly
+$ npx squint run src/my_app/core.cljs
+
+# Or compile separately and run with Node
+$ npx squint compile src/my_app/core.cljs
+$ node out/my_app/core.mjs
+ +

Make sure your package.json has + "type": "module" (see below).

+ +

package.json setup

+ +

The most important setting for Squint projects:

+ +
{
+  "type": "module"
+}
+ +

Squint outputs .mjs files by default, which Node.js + treats as ES modules regardless of this setting. But if you + configure Squint to output .js files (via + :extension "js" in squint.edn), or if + other tooling expects it, "type": "module" is + required. It's good practice to always include it.

+ +

A complete package.json for a Squint project:

+ +
{
+  "name": "my-squint-project",
+  "type": "module",
+  "scripts": {
+    "dev": "squint watch",
+    "build": "squint compile src/**/*.cljs",
+    "start": "node out/my_app/core.mjs"
+  },
+  "dependencies": {
+    "squint-cljs": "latest"
+  }
+}
+ +

Publishing to npm

+ +

Squint libraries can be published to npm as standard ES modules. + JavaScript consumers don't need to know the code was written in + Squint:

+ +
{
+  "name": "my-library",
+  "type": "module",
+  "exports": {
+    ".": "./out/my_library/core.mjs"
+  },
+  "files": ["out"]
+}
+ +

Compile before publishing:

+ +
$ npx squint compile src/**/*.cljs
+$ npm publish
+ +

Consumers install and use it like any npm package — the compiled + output is clean, readable JavaScript with minimal overhead.

+ +
+
+ + + + diff --git a/site/cookbook.html b/site/cookbook.html new file mode 100644 index 00000000..7a158e27 --- /dev/null +++ b/site/cookbook.html @@ -0,0 +1,236 @@ + + + + + + + + + Cookbook — Squint + + + + +
+

Cookbook

+ +

Practical recipes for common tasks in Squint. Each recipe is + self-contained — copy, adapt, and use.

+ +
+ Recipes + +
+ + + +

CLI script with argument parsing

+ +
(ns cli
+  (:require ["process" :as process]))
+
+(defn parse-args [argv]
+  (let [args (.slice argv 2)]
+    {:command (first args)
+     :flags   (filter #(.startsWith % "--") args)
+     :rest    (remove #(.startsWith % "--") (rest args))}))
+
+(let [{:keys [command flags rest]} (parse-args process/argv)]
+  (case command
+    "greet" (println "Hello," (first rest))
+    "help"  (println "Usage: cli [greet|help] [name]")
+    (println "Unknown command:" command)))
+ +
$ npx squint run cli.cljs greet Alice
+Hello, Alice
+ + + +

HTTP server with Node.js

+ +
(ns server
+  (:require ["http" :as http]))
+
+(defn handler [req res]
+  (let [url (.-url req)
+        method (.-method req)]
+    (println method url)
+    (.writeHead res 200 {:Content-Type "text/plain"})
+    (.end res (str "Hello from Squint! You requested " url))))
+
+(def port 3000)
+
+(-> (http/createServer handler)
+    (.listen port
+      (fn [] (println (str "Listening on http://localhost:" port)))))
+ + + +

Cloudflare Worker

+ +
(ns worker)
+
+(defn ^:async handle-request [request env ctx]
+  (let [url (js/URL. (.-url request))
+        path (.-pathname url)]
+    (case path
+      "/"
+      (js/Response. "Hello from Squint on the edge!"
+        {:headers {:content-type "text/plain"}})
+
+      "/api/time"
+      (js/Response.
+        (js/JSON.stringify {:time (.toISOString (js/Date.))})
+        {:headers {:content-type "application/json"}})
+
+      ;; default
+      (js/Response. "Not found" {:status 404}))))
+
+(def default {:fetch handle-request})
+ +

Deploy with wrangler deploy. Bundle size: typically + under 5kb.

+ + + +

React component

+ +
(ns app
+  (:require ["react" :as react]))
+
+(defn TodoApp []
+  (let [[items setItems] (react/useState [])
+        [input setInput] (react/useState "")
+        add-item (fn []
+                   (when (seq input)
+                     (setItems (conj items {:text input
+                                               :done false}))
+                     (setInput "")))]
+    #jsx [:div
+      [:h1 "Todo List"]
+      [:div
+        [:input {:value input
+                 :onChange #(setInput (.-value (.-target %)))
+                 :onKeyDown #(when (= (.-key %) "Enter") (add-item))}]
+        [:button {:onClick add-item} "Add"]]
+      [:ul
+        (map-indexed
+          (fn [i {:keys [text done]}]
+            #jsx [:li {:key i} text])
+          items)]]))
+ + + +

File processing pipeline

+ +
(ns process-csv
+  (:require ["fs/promises" :as fs]
+            ["path" :as path]))
+
+(defn parse-csv [text]
+  (let [lines (.split (.trim text) "\n")
+        headers (.split (first lines) ",")]
+    (map (fn [line]
+           (zipmap headers (.split line ",")))
+         (rest lines))))
+
+(defn ^:async main []
+  (let [text (js-await (fs/readFile "data.csv" "utf-8"))
+        rows (parse-csv text)
+        summary (->> rows
+                     (group-by #(get % "category"))
+                     (map (fn [[k v]] {:category k :count (count v)}))
+                     (sort-by :count)
+                     (reverse))]
+    (doseq [{:keys [category count]} summary]
+      (println category ":" count))))
+
+(main)
+ + + +

GitHub Action script

+ +
(ns check-pr
+  (:require ["process" :as process]))
+
+(defn ^:async main []
+  (let [token (aget process/env "GITHUB_TOKEN")
+        repo  (aget process/env "GITHUB_REPOSITORY")
+        url   (str "https://api.github.com/repos/" repo "/pulls?state=open")
+        res   (js-await (js/fetch url
+                {:headers {:Authorization (str "token " token)}}))
+        prs   (js-await (.json res))]
+    (println (count prs) "open PRs")
+    (doseq [pr prs]
+      (println "  -" (.-title pr)))))
+
+(main)
+ + + +

Consuming a JSON API

+ +
(ns weather)
+
+(defn ^:async get-weather [city]
+  (let [url (str "https://wttr.in/" city "?format=j1")
+        res (js-await (js/fetch url))
+        data (js-await (.json res))
+        current (first (.-current_condition data))]
+    {:temp (.-temp_C current)
+     :feels-like (.-FeelsLikeC current)
+     :desc (.-value (first (.-weatherDesc current)))}))
+ + + +

Simple testing

+ +
(ns test
+  (:require ["assert" :as assert]))
+
+(defn test-basics []
+  (assert/strictEqual (+ 1 2) 3)
+  (assert/deepStrictEqual
+    (vec (map inc [1 2 3]))
+    [2 3 4])
+  (assert/strictEqual
+    (:name {:name "Alice"})
+    "Alice")
+  (println "All tests passed!"))
+
+(test-basics)
+ +
$ npx squint run test.cljs
+All tests passed!
+ +

For larger projects, consider using Node's built-in test runner + (node:test) or a JS testing framework like Vitest.

+ +
+
+ + + + diff --git a/site/editors.html b/site/editors.html new file mode 100644 index 00000000..0381d20f --- /dev/null +++ b/site/editors.html @@ -0,0 +1,144 @@ + + + + + + + + + Editor & nREPL Setup — Squint + + + + +
+

Editor & nREPL Setup

+ +

Squint works with any text editor — .cljs files get + Clojure syntax highlighting out of the box. For a richer experience + with inline evaluation, use nREPL integration.

+ +
+ Contents + +
+ +

nREPL server

+ +

Squint includes an experimental nREPL server that enables editor + integration:

+ +
$ npx squint nrepl-server :port 1339
+ +

This starts an nREPL server on port 1339. Your editor's Clojure + plugin can connect to this port for inline evaluation, code + completion, and interactive development.

+ +

Note: The nREPL server is experimental. Known limitation: + when sending multiple forms at once, some may not be evaluated. + Send forms one at a time for reliable results.

+ +

VS Code (Calva)

+ +

Calva is the most popular Clojure + extension for VS Code. To use with Squint:

+ +
    +
  1. Install the Calva extension from the VS Code marketplace.
  2. +
  3. Start the nREPL server: npx squint nrepl-server :port 1339
  4. +
  5. In VS Code, run Calva: Connect to a Running REPL Server + from the command palette.
  6. +
  7. Select "Generic" as the project type.
  8. +
  9. Enter the port (1339).
  10. +
+ +

You can now evaluate expressions inline with + Ctrl+Enter (current form) or + Alt+Enter (top-level form).

+ +

Emacs (CIDER)

+ +

CIDER can connect to Squint's + nREPL server:

+ +
    +
  1. Start the nREPL server: + npx squint nrepl-server :port 1339
  2. +
  3. In Emacs, run M-x cider-connect.
  4. +
  5. Enter localhost and port 1339.
  6. +
+ +

Standard CIDER keybindings work: C-c C-e to evaluate + the expression before point. Note: C-c C-k (evaluate + buffer) sends multiple forms at once, which may be unreliable due + to the nREPL limitation above. Prefer evaluating forms one at a + time.

+ +

Neovim (Conjure)

+ +

Conjure connects + to nREPL for inline evaluation in Neovim:

+ +
    +
  1. Install Conjure via your plugin manager.
  2. +
  3. Start the nREPL server: + npx squint nrepl-server :port 1339
  4. +
  5. Create a .nrepl-port file containing + 1339 in your project root.
  6. +
  7. Open a .cljs file — Conjure auto-connects.
  8. +
+ +

IntelliJ (Cursive)

+ +

Cursive provides Clojure + support in IntelliJ. It can connect to Squint's nREPL via the + "Remote REPL" configuration:

+ +
    +
  1. Start the nREPL server.
  2. +
  3. Create a new "Clojure REPL → Remote" run configuration.
  4. +
  5. Set host to localhost, port to 1339.
  6. +
+ +

Console REPL

+ +

For quick experimentation without editor integration:

+ +
$ npx squint repl
+ +

This starts an interactive REPL in your terminal. You can also + use the web-based playground + for a richer experience with syntax highlighting, compiled output + viewing, and sharing.

+ +

Syntax highlighting without nREPL

+ +

Even without nREPL, most editors highlight .cljs files + correctly. If yours doesn't, configure it to use Clojure syntax + highlighting for .cljs files. The experience will be + similar to editing ClojureScript — the syntax is identical.

+ +
+
+ + + + diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 00000000..67d14394 --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/site/from-clojure.html b/site/from-clojure.html new file mode 100644 index 00000000..175cfec0 --- /dev/null +++ b/site/from-clojure.html @@ -0,0 +1,248 @@ + + + + + + + + + Squint from Clojure — Squint + + + + +
+

Squint from Clojure

+ +

If you already know Clojure or ClojureScript, this guide covers + everything that's different in Squint. If something isn't mentioned + here, it probably works the way you'd expect.

+ +
+ Contents + +
+ +

What's the same

+ +

Most of the syntax you know works unchanged:

+ + + +

Data structures are JavaScript types

+ +

This is the biggest difference. Squint maps are JavaScript objects + and vectors are JavaScript arrays — but the standard library preserves + Clojure-like semantics where possible.

+ +

Non-bang functions return shallow copies (like Clojure):

+ +
(def m {:a 1})
+(def m2 (assoc m :b 2))
+;; m  => {:a 1}       (unchanged — shallow copy made)
+;; m2 => {:a 1 :b 2}  (new object)
+ +

Bang functions mutate in place (for performance):

+ +
(def m {:a 1})
+(assoc! m :b 2)
+;; m => {:a 1 :b 2}  (mutated in place)
+ +

This applies to assoc/assoc!, + dissoc/dissoc!, + conj/conj!, etc. Use the non-bang version + when you want Clojure-like copy semantics. Use the bang version when + you need performance and know mutation is safe.

+ +

The key difference from Clojure: shallow copies don't use structural + sharing, so copying large collections is more expensive than in Clojure. + Also, nested objects still share references:

+ +
(let [m {:a {:b 1}}
+      m2 (assoc m :c 2)]
+  (identical? (:a m) (:a m2)))
+;; true — nested objects are shared, not deep-copied
+ +

Equality

+ +

Squint's = implements deep equality, matching + Clojure's semantics — it does not simply compile to JavaScript's + ===:

+ +
(= {:a 1} {:a 1})   ; true  — deep comparison
+(= [1 2] [1 2])     ; true  — element-wise comparison
+(= 1 1)             ; true  — primitives
+(= :a "a")          ; true  — keywords ARE strings
+ +

Use identical? if you need reference equality (JavaScript + ===):

+ +
(def m {:a 1})
+(identical? m m)               ; true  — same object
+(identical? {:a 1} {:a 1})  ; false — different objects
+ +

Keywords are strings

+ +

In Clojure, keywords are their own type. In Squint, :foo + compiles to the string "foo". This means:

+ + + +

Namespaces and requires

+ +

The ns form compiles to ES module imports. There are two + kinds of requires:

+ +
(ns my-app
+  (:require
+    ;; String require → npm/Node package
+    ["react" :as react]
+    ["fs" :refer [readFileSync]]
+
+    ;; Symbol require → other Squint namespace
+    [my-app.utils :as utils]))
+ +

Unlike Clojure, there's no :use or :import. + Macros must be loaded via :require-macros with + :refer — using plain :require will import + them as regular functions instead of expanding at compile time. + See the Macro Guide for details.

+ +

What's missing

+ +

Features from Clojure/ClojureScript that are not available + in Squint:

+ + + +

What's added

+ +

Features in Squint that don't exist in standard Clojure:

+ + + +

Also note: collections cannot be used as functions. In Clojure + you can write ({:a 1} :a) to look up a key. In Squint, + you must use (get {:a 1} :a) or (:a {:a 1}) + (keywords as functions work).

+ +

Another difference: map keys must be strings. Since Squint maps + are JavaScript objects, only string keys are supported. Using a vector + or other non-string as a map key will silently stringify it.

+ +

Gotchas

+ +

Surprises for experienced Clojure developers:

+ + + +
+
+ + + + diff --git a/site/fun.cljs b/site/fun.cljs new file mode 100644 index 00000000..6e413680 --- /dev/null +++ b/site/fun.cljs @@ -0,0 +1,4 @@ +(defn greet [name] + (str "Hello, " name "!")) + +(println (greet "Alice")) diff --git a/site/index.html b/site/index.html new file mode 100644 index 00000000..71c4ae43 --- /dev/null +++ b/site/index.html @@ -0,0 +1,331 @@ + + + + + + + + + Squint — Clojure syntax, JavaScript output + + + +
+ + + +

+ +

+ +

Squint is a light-weight dialect that brings together the + expressiveness of Clojure with + the reach of JavaScript + — compiling Clojure syntax to lean, readable JS.

+ + + +

Anywhere you can run JavaScript, you can run Squint.

+ + + +

Get started now!

+ + + +
+
+

Squint

+
(ns app
+  (:require ["fs/promises" :as fs]))
+
+(defn ^:async read-config [path]
+  (let [text (js-await (fs/readFile path "utf-8"))
+        {:keys [host port]} (js/JSON.parse text)]
+    {:host host
+     :port (or port 3000)}))
+
+(defn handler [{:keys [method url]}]
+  (println method url)
+  (js/Response. "OK" {:status 200}))
+
+
+

JavaScript

+
import * as fs from "fs/promises";
+
+async function readConfig(path) {
+  const text = await fs.readFile(path, "utf-8");
+  const { host, port } = JSON.parse(text);
+  return { host, port: port ?? 3000 };
+}
+
+function handler({ method, url }) {
+  console.log(method, url);
+  return new Response("OK", { status: 200 });
+}
+
+
+ +

The JavaScript shown is simplified for clarity — the actual + compiler output uses a small runtime for Clojure semantics + (deep equality, truthiness, etc.). See exact output in the + playground.

+ + + +

Don't feel like installing it?

+ +

Try Squint right here — type an expression and press Enter:

+ +
+
+ + +
+ + + +
+
+
+ +
+
+ +

For a richer experience with syntax highlighting and sharing, + use the full playground.

+ + + +

Getting started

+ +
$ mkdir my-project && cd my-project
+$ npm init -y
+$ npm install squint-cljs@latest
+ +

Create hello.cljs:

+ +
(ns hello)
+
+(println "Hello from Squint!")
+
+(defn greet [name]
+  (str "Hello, " name "!"))
+
+(println (greet "world"))
+ +

Run it:

+ +
$ npx squint run hello.cljs
+Hello from Squint!
+Hello, world!
+ +

To compile to .mjs files and watch for changes:

+ +
$ npx squint watch
+ +

Configure your project with a squint.edn file. + See the README + for full compiler options.

+ + + +

Documentation

+ + + + + +

Looking for full ClojureScript semantics?

+ +

If you need persistent immutable data structures and the full + ClojureScript standard library, check out + Cherry — + Squint's sibling project. Cherry uses the same compiler infrastructure + but preserves ClojureScript's immutable semantics, trading bundle size + for richer data structure guarantees. Cherry is more experimental and + not yet recommended for production use.

+ + + +

Community

+ + + +

Squint is created by + Michiel Borkent, + also known for Babashka, + clj-kondo, + and many other Clojure tools.

+ +
+
+ + + + + + + diff --git a/site/js-primer.html b/site/js-primer.html new file mode 100644 index 00000000..319c59ec --- /dev/null +++ b/site/js-primer.html @@ -0,0 +1,247 @@ + + + + + + + + + JavaScript Primer — Squint + + + + +
+

JavaScript Primer

+ +

While Squint shields you from most JavaScript syntax, you'll encounter + JS concepts in error messages, library documentation, and interop code. + This primer covers what you need to know about the runtime your code + targets.

+ +
+ Contents + +
+ +

Types and values

+ +

JavaScript has 7 primitive types and one complex type:

+ +
// Primitives
+42              // number (64-bit float, no int type)
+"hello"         // string
+true            // boolean
+null            // null (Squint's nil)
+undefined       // undefined (absent value)
+Symbol("id")    // symbol (unique identifier)
+42n             // bigint (arbitrary precision integer)
+
+// Complex type
+{ a: 1 }        // object (Squint's maps)
+ +

Truthiness: In raw JavaScript, 0, "", + null, undefined, NaN, and + false are all falsy. However, Squint follows + ClojureScript semantics: only nil (null), + undefined, and false are falsy. + 0 and "" are truthy in Squint. This means + (when count ...) will execute even when + count is 0. Be aware of this difference + when reading JavaScript documentation.

+ +

Numbers: JavaScript has only one number type: 64-bit floating + point. There's no integer type. This means (/ 10 3) gives + 3.3333..., not 3. Use + js/Math.floor for integer division.

+ +

Objects and arrays

+ +

These are the data structures that Squint maps and vectors compile to.

+ +
// Objects — Squint maps compile to these
+const person = { name: "Alice", age: 30 };
+person.name;          // "Alice"
+person["age"];        // 30
+person.email = "a@b"; // mutation — adds a key
+
+// Arrays — Squint vectors compile to these
+const nums = [1, 2, 3];
+nums[0];              // 1 (zero-indexed)
+nums.length;          // 3
+nums.push(4);         // mutation — adds to end
+ +

Objects are compared by reference, not by value: + { a: 1 } === { a: 1 } is false because + they're two separate objects in memory.

+ +

Functions

+ +

JavaScript has several function syntaxes. Squint's defn + compiles to function expressions assigned to variables, and + fn to function expressions (or arrow functions with + ^:=>):

+ +
// function expression (from defn)
+var greet = function(name) {
+  return "Hello, " + name;
+};
+
+// function expression (from fn)
+const add = function(a, b) { return a + b; };
+
+// arrow with body
+const process = (x) => {
+  const y = x * 2;
+  return y + 1;
+};
+ +

Key differences from Clojure functions: JS functions can be called with + any number of arguments without error (extras are ignored, missing ones + are undefined). They also have a this context + which matters for method calls.

+ +

Modules

+ +

Squint's ns/:require compiles to ES modules, + the modern JavaScript module system:

+ +
// Named imports
+import { readFile, writeFile } from "fs/promises";
+
+// Namespace import
+import * as path from "path";
+
+// Default import
+import React from "react";
+
+// Named exports (from defn)
+export function myFn() { ... }
+
+// Default export
+export default { fetch: handler };
+ +

ES modules are statically analyzable — imports and exports must be at + the top level, not inside conditionals. This is why Squint requires + all :require forms inside ns.

+ +

Promises and async

+ +

Promises are JavaScript's mechanism for asynchronous computation. + When you use ^:async and js-await in Squint, + you're working with promises.

+ +
// A promise represents a future value
+const promise = fetch("https://api.example.com/data");
+
+// async/await — what Squint compiles to
+async function getData() {
+  const response = await fetch("https://api.example.com/data");
+  const json = await response.json();
+  return json;
+}
+
+// An async function always returns a promise
+getData().then(data => console.log(data));
+ +

Key things to know:

+ + +

npm and Node.js

+ +

npm is the JavaScript package manager. When you run + npm install some-package, it downloads the package into a + node_modules directory and records the dependency in + package.json.

+ +

Node.js is the most common server-side JavaScript runtime. + It provides built-in modules like fs (file system), + path, http, and child_process. + You can require these in Squint with string requires:

+ +
(ns my-script
+  (:require ["fs" :as fs]
+            ["path" :as path]))
+ +

Other runtimes like Bun and Deno are largely compatible + with Node.js APIs and npm packages.

+ +

The browser and the DOM

+ +

In browsers, JavaScript can interact with the page via the DOM + (Document Object Model). You access it through global objects like + document and window:

+ +
;; In Squint
+(def el (js/document.getElementById "app"))
+(set! (.-textContent el) "Hello!")
+(.addEventListener el "click"
+  (fn [e] (println "Clicked!")))
+ +

Most modern web development uses frameworks (React, Vue, Svelte) rather + than direct DOM manipulation. Squint's JSX support makes it a natural fit + for React-based development.

+ +

JavaScript gotchas

+ + + +
+
+ + + + diff --git a/site/macros.html b/site/macros.html new file mode 100644 index 00000000..58aebf46 --- /dev/null +++ b/site/macros.html @@ -0,0 +1,207 @@ + + + + + + + + + Macro Guide — Squint + + + + +
+

Macro Guide

+ +

Macros are a defining feature of Lisp languages. They let you transform + code at compile time, extending the language with your own syntax. + Squint supports defmacro for compile-time code + generation.

+ +
+ Contents + +
+ +

Macro basics

+ +

A macro is a function that runs at compile time. It receives + unevaluated code as data, transforms it, and returns new code that gets + compiled in its place.

+ +
;; A simple macro that logs before executing
+(defmacro with-timing [label & body]
+  `(let [start# (js/Date.now)
+         result# (do ~@body)
+         elapsed# (- (js/Date.now) start#)]
+     (println ~label "took" elapsed# "ms")
+     result#))
+
+;; Usage
+(with-timing "fetch"
+  (js-await (js/fetch url)))
+ +

When the compiler encounters (with-timing ...), it calls + the macro function, which returns a let form that wraps + the body with timing code. The returned code is then compiled normally.

+ +

Quoting and unquoting

+ +

Syntax-quote (`) is the primary tool for building code + in macros:

+ +
;; ` (syntax-quote) — returns code as data, with namespace resolution
+`(+ 1 2)   ; => the list (+ 1 2)
+
+;; ~ (unquote) — evaluates within syntax-quote
+(let [x 42]
+  `(+ ~x 1))   ; => (+ 42 1)
+
+;; ~@ (unquote-splicing) — splices a list into the surrounding form
+(let [args [1 2 3]]
+  `(+ ~@args))   ; => (+ 1 2 3)
+
+;; # (auto-gensym) — generates unique symbol names
+`(let [x# 1] x#)   ; => (let [x__123 1] x__123)
+ +

The auto-gensym (#) suffix is important — it prevents + your macro's internal variables from colliding with user code. Always + use it for temporary bindings in macros.

+ +

Common patterns

+ +

Wrapping patterns — add behavior around existing code:

+ +
(defmacro unless [test & body]
+  `(when (not ~test) ~@body))
+
+;; Usage
+(unless (valid? input)
+  (println "invalid input!"))
+ +

Note: Macros that generate try/catch + forms using syntax-quote have a known compiler bug — the + catch clause may not compile correctly. Use + try/catch directly in your code instead + of inside macros.

+ +

Defining new binding forms:

+ +
(defmacro if-let* [bindings then else]
+  (if (empty? bindings)
+    then
+    (let [[sym val & rest] bindings]
+      `(let [temp# ~val]
+         (if temp#
+           (let [~sym temp#]
+             (if-let* ~(vec rest) ~then ~else))
+           ~else)))))
+ +

Loading macros

+ +

Define macros in a separate file and load them with + :require-macros:

+ +
;; src/my_app/macros.cljs
+(ns my-app.macros)
+
+(defmacro with-timing [label & body]
+  `(let [start# (js/Date.now)
+         result# (do ~@body)
+         elapsed# (- (js/Date.now) start#)]
+     (println ~label "took" elapsed# "ms")
+     result#))
+
+;; src/my_app/main.cljs — load via :require-macros
+(ns my-app.main
+  (:require-macros [my-app.macros :refer [with-timing]]))
+ +

Important: Use :require-macros, not plain + :require. Plain :require will import the + macro file as a regular module instead of expanding macros at compile + time.

+ +

Macros can also be defined in the same file where they're used, but + note that the macro definition itself will be emitted into the compiled + output, which may cause runtime errors. Separate macro files are the + recommended approach.

+ +

Macros are resolved at compile time. The compiler uses + SCI (Small Clojure + Interpreter) to evaluate macro definitions, then uses them to expand + macro calls in the requiring file.

+ +

Note: Cross-namespace macro resolution has some known + limitations — qualified macro calls and aliased namespaces may not + resolve correctly in all cases. Keep macros simple and test + thoroughly.

+ +

Macros vs. functions

+ +

Use functions by default. Only reach for macros when you need to:

+ + + +

If a function can do the job, prefer it. Functions are simpler to + understand, test, and compose. Macros add power but also cognitive + overhead.

+ +

Debugging macros

+ +

When a macro doesn't produce the expected output:

+ +
    +
  1. Check the expansion. Use macroexpand or + macroexpand-1 to see what code your macro generates.
  2. +
  3. Print during compilation. Adding (println ...) + inside defmacro (outside the syntax-quote) will print + at compile time.
  4. +
  5. Check for missing gensyms. If you get "variable not found" + errors, you probably forgot a # suffix on a temporary + binding.
  6. +
  7. Check for missing unquotes. If a variable name appears + literally in the output instead of its value, you forgot a + ~.
  8. +
+ +
+
+ + + + diff --git a/site/porting.html b/site/porting.html new file mode 100644 index 00000000..53cb8683 --- /dev/null +++ b/site/porting.html @@ -0,0 +1,226 @@ + + + + + + + + + Porting from ClojureScript — Squint + + + + +
+

Porting from ClojureScript

+ +

This guide walks through migrating an existing ClojureScript project + to Squint. It's based on real-world porting experiences and covers + the common issues you'll encounter.

+ +
+ Contents + +
+ +

Assess your project

+ +

Not every ClojureScript project is a good candidate for porting. + Squint works best when:

+ + + +

Projects that are poor candidates: large apps using Reagent/ + Re-frame (use Reagami + instead), anything relying on core.async, or code-sharing with + Clojure JVM via .cljc.

+ +

Project setup

+ +

Create a fresh npm project alongside your existing CLJS project:

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

Create a squint.edn:

+ +
{:paths ["src"]
+ :output-dir "out"}
+ +

Copy your .cljs source files into src/. + Then start the watch loop and fix errors iteratively:

+ +
$ npx squint watch
+ +

Convert namespaces

+ +

The ns form needs adjustments for JS packages:

+ +
;; Before (ClojureScript)
+(ns my-app.core
+  (:require [reagent.core :as r]
+            [clojure.string :as str]
+            [my-app.utils :as utils])
+  (:import [goog.string format]))
+
+;; After (Squint)
+(ns my-app.core
+  (:require ["react" :as react]          ; npm package
+            [clojure.string :as str]  ; built-in
+            [my-app.utils :as utils])) ; Squint ns
+ +

Key changes:

+ + +

Data structure changes

+ +

Most code works without changes because Squint's non-bang functions + (assoc, conj, etc.) return shallow copies, + approximating Clojure's value semantics. Note: unlike Clojure's + structurally-shared persistent data structures, Squint uses shallow + cloning — nested objects share references. Watch for these cases:

+ +
;; Remove clj->js / js->clj — not needed, everything is JS
+;; Before:
+(clj->js {:a 1})
+;; After:
+{:a 1}  ; already a JS object
+
+;; Collections as functions — must use get
+;; Before:
+(my-map :key)
+;; After:
+(:key my-map)    ; keyword-as-function works
+(get my-map :key)  ; or explicit get
+
+;; Non-string map keys — won't work
+;; Before:
+{[1 2] "value"}    ; CLJS supports this
+;; After: restructure to use string keys, or use js/Map
+;; (but note: Squint's get/assoc don't work on js/Map —
+;; use .get/.set methods directly)
+ +

Simplify interop

+ +

One of the biggest wins of porting: all the clj->js, + js->clj, and #js ceremony disappears:

+ +
;; Before (ClojureScript)
+(let [opts (clj->js {:method "POST"
+                      :headers {:Content-Type "application/json"}
+                      :body (.stringify js/JSON (clj->js data))})]
+  (.then (js/fetch url opts) ...))
+
+;; After (Squint) — much cleaner
+(defn ^:async post-data [url data]
+  (let [res (js-await
+              (js/fetch url
+                {:method "POST"
+                 :headers {:Content-Type "application/json"}
+                 :body (js/JSON.stringify data)}))]
+    (js-await (.json res))))
+ +

Replace libraries

+ +

ClojureScript libraries won't work in Squint. Common replacements:

+ + + +

Check the squint-macros + project for compatibility macros that ease the transition.

+ +

Test the port

+ +

After converting, test incrementally:

+ +
# Compile everything
+$ npx squint compile src/**/*.cljs
+
+# Run your entry point
+$ node out/my_app/core.mjs
+
+# Or use squint run for quick iteration
+$ npx squint run src/my_app/core.cljs
+ +

Common issues at this stage:

+ + +

Porting checklist

+ + + +
+
+ + + + diff --git a/site/rationale.html b/site/rationale.html new file mode 100644 index 00000000..75802ddc --- /dev/null +++ b/site/rationale.html @@ -0,0 +1,183 @@ + + + + + + + + + Rationale — Squint + + + + +
+

Rationale

+ +

Why does Squint exist? ClojureScript already compiles Clojure to + JavaScript. What problem does Squint solve differently?

+ +
+ Contents + +
+ +

The problem

+ +

ClojureScript is a remarkable achievement — a full Clojure implementation + targeting JavaScript. But its design priorities create friction in certain + environments:

+ + + +

Squint's approach

+ +

Squint takes a different path: use only native JavaScript data + structures. This single decision cascades through the entire design:

+ + + +

The result: Squint gives you Clojure's expressive syntax — its + destructuring, its threading macros, its let bindings, + its namespaces — on top of JavaScript's data model.

+ +

The trade-offs

+ +

This is not a free lunch. By choosing native JS data structures, Squint + gives up some of Clojure's core guarantees:

+ + + +

These are real trade-offs. Whether they matter depends entirely on your + use case.

+ +

When to use Squint

+ + + +

When not to use Squint

+ + + +

What about Cherry?

+ +

Cherry is Squint's + sibling project, built on the same compiler infrastructure. Where Squint + chooses native JS types, Cherry preserves ClojureScript's immutable + persistent data structures.

+ +

Think of them as two points on a spectrum:

+ + + +

Cherry is currently experimental and not recommended for production use. + If you need immutable data and are willing to accept a larger bundle, + keep an eye on its development.

+ +
+
+ + + + diff --git a/site/squint.css b/site/squint.css new file mode 100644 index 00000000..de1779cb --- /dev/null +++ b/site/squint.css @@ -0,0 +1,421 @@ +:root { + /* ── Gray (neutral) ──────────────────────────────────── + Identical to Fennel's grays — perceptually uniform lightness steps. + Contrast ratios (light mode, on --gray-99 white): + gray-10: 16.9:1 | gray-15: 15.1:1 | gray-25: 11.2:1 + gray-38: 7.0:1 | gray-50: 4.5:1 | gray-60: 3.1:1 */ + --gray-10: #1b1d1c; + --gray-15: #262626; + --gray-25: #3b3b3b; + --gray-38: #595959; + --gray-50: #777777; + --gray-60: #919191; + --gray-70: #ababab; + --gray-85: #d4d4d4; + --gray-95: #f0f0f0; + --gray-99: #ffffff; + + /* ── Amber/Orange (primary accent) ───────────────────── + Derived from Squint logo #EA701D, hue ~65 OKLCH. + Chroma peaks around values 50-70. */ + --amber-10: #2a1800; + --amber-15: #3a2200; + --amber-25: #573600; + --amber-38: #7c5200; + --amber-50: #9e6e10; + --amber-60: #be8a2a; + --amber-70: #d9a84e; + --amber-85: #f2d08e; + --amber-95: #fcedc8; + --amber-99: #fffbf2; + + /* ── Blue (links) ────────────────────────────────────── + Hue 245-227, same family as Fennel. */ + --blue-10: #00202a; + --blue-15: #032a3e; + --blue-25: #00415d; + --blue-38: #00627f; + --blue-50: #147da3; + --blue-60: #1e9ec1; + --blue-70: #68b6d9; + --blue-85: #a2def5; + --blue-95: #d3f2ff; + --blue-99: #f4fdff; + + /* ── Teal (syntax: values, strings, keywords) ────────── + Hue ~195, low-mid chroma. */ + --teal-10: #002020; + --teal-15: #002d2d; + --teal-25: #004545; + --teal-38: #006565; + --teal-50: #1a8585; + --teal-60: #35a2a2; + --teal-70: #5cbcbc; + --teal-85: #98dede; + --teal-95: #ccf0f0; + --teal-99: #f2fefe; + + /* ── Warm brown (code backgrounds) ───────────────────── + Very low chroma, warm-shifted gray. */ + --warm-10: #221c18; + --warm-15: #2d2621; + --warm-99: #fffcf9; + + /* ── Semantic mappings (light mode) ──────────────────── */ + --base-bg: var(--gray-99); + --base-bg-in: var(--gray-95); + --base-bg-mid: var(--gray-70); + --base-fg: var(--gray-10); + + --link-reg: var(--blue-25); + --link-hov: var(--blue-15); + --link-vis: var(--blue-38); + + --accent-main: var(--amber-38); + --accent-bold: var(--amber-25); + + --source-bg: var(--warm-99); + --source-fg: var(--gray-10); + --source-comment: var(--gray-50); + --source-value: var(--teal-38); + --source-special: var(--amber-38); + --source-macro: var(--blue-38); + --source-symbol: var(--gray-25); +} + +@media (prefers-color-scheme: dark) { + :root { + --base-bg: var(--gray-10); + --base-bg-in: var(--gray-15); + --base-bg-mid: var(--gray-25); + --base-fg: var(--gray-95); + + --link-reg: var(--blue-60); + --link-hov: var(--blue-70); + --link-vis: var(--blue-50); + + --accent-main: var(--amber-60); + --accent-bold: var(--amber-70); + + --source-bg: var(--warm-10); + --source-fg: var(--gray-95); + --source-comment: var(--gray-60); + --source-value: var(--teal-70); + --source-special: var(--amber-70); + --source-macro: var(--blue-70); + --source-symbol: var(--gray-85); + } +} + +/* ── Layout & typography ─────────────────────────────── */ + +html { + color: var(--base-fg); + background-color: var(--base-bg); +} + +body { + font-family: "Fira Sans", "Helvetica Neue", Helvetica, Calibri, Verdana, sans-serif; + margin: 40px auto; + padding: 1em; + max-width: 44em; + line-height: 1.6; + font-size: 14pt; +} + +h1 { + text-align: center; + font-size: 2.4em; + margin-bottom: 0.2em; +} + +h1 svg.logo { + max-width: 300px; + height: auto; + display: block; + margin: 0 auto; +} + +h1 svg { + max-width: 1.6em; + height: auto; + vertical-align: -0.15em; +} + +h2 { + margin-top: 2.5em; + color: var(--accent-bold); +} + +h3 { + margin: 0 0 0.3em 0; + font-size: 11pt; + color: var(--accent-main); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +img { + max-width: 100%; +} + +/* ── Links ───────────────────────────────────────────── */ + +a { + color: var(--link-reg); +} + +a:hover { + color: var(--link-hov); +} + +a:visited { + color: var(--link-vis); +} + +:focus { + outline: 4px solid var(--accent-main); + outline-offset: 4px; +} + +/* ── Code & monospace ────────────────────────────────── */ + +tt, pre, code, kbd { + font-family: "Fira Mono", Inconsolata, monospace; +} + +code { + background-color: var(--base-bg-in); + padding: 0.1em 0.3em; + border-radius: 3px; + font-size: 0.9em; +} + +.code { + border-radius: 3px; + border: 1px solid var(--base-bg-mid); + font-size: 12pt; + line-height: 1.5; + margin: 1.5em 0; + padding: 1em; + text-align: left; + white-space: pre-wrap; + overflow-x: auto; + background-color: var(--source-bg); + color: var(--source-fg); +} + +/* ── Syntax highlighting ─────────────────────────────── */ + +.code .comment { color: var(--source-comment); font-style: italic; } +.code .keyword { color: var(--source-value); } +.code .string { color: var(--source-value); } +.code .number { color: var(--source-value); } +.code .special { color: var(--source-special); font-weight: bold; } +.code .macro { color: var(--source-macro); } +.code .symbol { color: var(--source-symbol); } + +.code .js-keyword { color: var(--source-special); font-weight: bold; } +.code .js-string { color: var(--source-value); } +.code .js-number { color: var(--source-value); } +.code .js-comment { color: var(--source-comment); font-style: italic; } + +/* ── Side-by-side code comparison ────────────────────── */ + +.code-compare { + display: flex; + gap: 1em; + margin: 2em 0; +} + +.code-pane { + flex: 1; + min-width: 0; +} + +.code-pane .code { + margin-top: 0; +} + +@media (max-width: 700px) { + .code-compare { + flex-direction: column; + } +} + +/* ── "Where" inline list ─────────────────────────────── */ + +#where { + list-style: none; + padding: 0; + text-align: center; +} + +#where li { + display: inline-block; + margin: 0.3em 1em; +} + +/* ── Feature list styling ────────────────────────────── */ + +.features { + padding-left: 1.2em; +} + +.features li { + margin-bottom: 0.6em; +} + +/* ── Install / getting started ───────────────────────── */ + +.shell { + background-color: var(--base-bg-in); + color: var(--base-fg); +} + +/* ── Interactive REPL ─────────────────────────────────── */ + +.outputs { + display: flex; + width: 100%; + align-items: stretch; + min-height: 16em; +} + +.code-flex { + display: flex; + flex-direction: column; + flex-grow: 1; + width: min-content; + min-height: 16em; +} + +#repl-console, #compiled-js { + overflow-y: auto; + padding: 1em 1.2em; + display: block; + flex-grow: 1; + max-height: 20em; +} + +#repl-console pre { + margin: 0; + font-size: 11pt; + white-space: pre-wrap; +} + +#repl-console .input { color: var(--accent-main); } +#repl-console .error { color: #c44; } +#repl-console .info { color: var(--source-comment); font-style: italic; } +#repl-console .result { color: var(--source-value); } + +.repl-input-container { + display: flex; + border-top: 1px solid var(--base-bg-mid); +} + +.repl-input-container > * { + outline: none; + border: none; + white-space: pre-wrap; + font-family: "Fira Mono", Inconsolata, monospace; + color: var(--base-fg); + background: var(--base-bg-in); + min-height: 1em; + padding: 0.8em; + margin: 0; +} + +#repl-prompt { + padding-right: 0; + font-weight: bold; + color: var(--accent-main); +} + +#repl-input { + flex: 1; + font-size: 11pt; + padding-left: 0.3em; + resize: none; + border-radius: 0 0 0 3px; +} + +#toggle-compiled-code { + font-size: 10pt; + cursor: pointer; + border-radius: 0 0 3px 0; + border-left: 1px solid var(--base-bg-mid); + color: var(--link-reg); +} + +#toggle-compiled-code:hover { + background: var(--base-bg); +} + +#js-pane { + display: none; +} + +@media (max-width: 700px) { + .outputs { + flex-direction: column; + } +} + +/* ── Doc page navigation ─────────────────────────────── */ + +nav { + font-size: 0.9em; + margin-bottom: 2em; + padding-bottom: 0.5em; + border-bottom: 1px solid var(--base-bg-mid); +} + +nav a { + margin-right: 1.5em; + text-decoration: none; +} + +nav a:hover { + text-decoration: underline; +} + +nav .nav-home { + font-weight: bold; + color: var(--accent-main); +} + +/* ── Table of contents (doc pages) ───────────────────── */ + +.toc { + background-color: var(--base-bg-in); + border-radius: 3px; + padding: 1em 1.5em; + margin: 1.5em 0; +} + +.toc ul { + margin: 0.3em 0; + padding-left: 1.2em; +} + +.toc li { + margin: 0.2em 0; +} + +/* ── Footer ──────────────────────────────────────────── */ + +footer { + color: var(--gray-50); + font-size: 0.9em; +} + +footer a { + color: var(--gray-50); +} + +hr { + margin-top: 2em; + border: none; + border-top: 1px solid var(--base-bg-mid); +} diff --git a/site/style.html b/site/style.html new file mode 100644 index 00000000..bc0be6f0 --- /dev/null +++ b/site/style.html @@ -0,0 +1,183 @@ + + + + + + + + + Style Guide — Squint + + + + +
+

Style Guide

+ +

Squint inherits most of its style conventions from Clojure. This guide + covers the conventions specific to Squint and areas where JavaScript + interop creates new questions.

+ +
+ Contents + +
+ +

Naming

+ +

Follow Clojure conventions:

+ + + +

Squint automatically converts kebab-case to camelCase in the compiled + output, so fetch-data becomes fetchData in JS. + This means you can use idiomatic Clojure names and get idiomatic JS + output.

+ +

Formatting

+ +

Standard Clojure formatting applies:

+ +
;; Good — aligned arguments
+(defn process-user [{:keys [name email role]}]
+  (let [normalized (-> name
+                      (.trim)
+                      (.toLowerCase))]
+    {:name normalized
+     :email email
+     :role (or role "user")}))
+
+;; Bad — cramped, inconsistent
+(defn process-user [{:keys [name email role]}]
+(let [normalized (.toLowerCase (.trim name))]
+{:name normalized :email email :role (or role "user")}))
+ + + +

Namespace organization

+ +
(ns my-app.handlers
+  (:require
+    ;; Node/npm packages first
+    ["express" :as express]
+    ["pg" :refer [Pool]]
+
+    ;; Then project namespaces
+    [my-app.db :as db]
+    [my-app.utils :refer [parse-id validate]]))
+ +

Group requires: external packages first, then project namespaces. + Prefer :as over :refer for external packages + to make it clear where functions come from.

+ +

Interop style

+ +

When to use Squint's standard library vs. direct JS interop:

+ +
;; Prefer: Squint stdlib when it exists
+(map inc [1 2 3])
+(assoc m :key value)
+(filter odd? nums)
+
+;; Also fine: JS methods for simple operations
+(.toUpperCase s)
+(.slice arr 0 5)
+(.startsWith path "/")
+
+;; Prefer: JS methods when there's no Squint equivalent
+(.padStart s 10 "0")
+(js/Object.entries m)
+(js/JSON.stringify data)
+ +

Use Squint's stdlib when it exists — it's more idiomatic and consistent. + Reach for JS methods when Squint doesn't provide an equivalent, or when + the JS method is simpler for the task at hand.

+ +

Async code

+ +
;; Good — clear async boundary
+(defn ^:async fetch-user [id]
+  (let [response (js-await (js/fetch (str "/api/users/" id)))]
+    (when (.-ok response)
+      (js-await (.json response)))))
+
+;; Good — error handling at the boundary
+(defn ^:async main []
+  (try
+    (let [user (js-await (fetch-user 42))]
+      (process user))
+    (catch e
+      (println "Error:" (.-message e)))))
+ +

Mark functions as ^:async only when they need to + js-await. Don't make everything async "just in case" — + it adds overhead and complicates the return type.

+ +

Working with mutability

+ +

Since Squint's data structures are mutable, adopt defensive habits:

+ + + +
;; Defensive pattern — clone before modifying
+(defn with-defaults [opts]
+  (merge {:timeout 5000
+          :retries 3}
+         opts))
+ +
+
+ + + + diff --git a/site/tutorial.html b/site/tutorial.html new file mode 100644 index 00000000..2c221fb6 --- /dev/null +++ b/site/tutorial.html @@ -0,0 +1,359 @@ + + + + + + + + + Tutorial — Squint + + + + +
+

Tutorial

+ +

This document will guide you through the basics of Squint. You should + have Squint installed to follow + along, or use the playground.

+ +
+ Contents + +
+ +

A note on JavaScript examples: Throughout this tutorial, + JavaScript output is shown in simplified form to aid + understanding. The actual compiler output uses a small runtime + library (squint_core) for Clojure semantics and may + differ in variable naming and code structure. Use the + playground + to see exact compiler output.

+ + + +

Hello World

+ +

Create a file called hello.cljs:

+ +
(ns hello)
+
+(println "Hello, world!")
+ +

Run it with:

+ +
$ npx squint run hello.cljs
+Hello, world!
+ +

The ns form declares the namespace — it's how Squint knows + what module you're defining. Every file should start with one.

+ + + +

Values and types

+ +

Squint compiles to JavaScript, so all values are JavaScript values:

+ +
42              ; number
+3.14            ; number (JS has no int/float distinction)
+"hello"         ; string
+true            ; boolean
+false           ; boolean
+nil             ; compiles to null
+:foo            ; keyword — compiles to the string "foo"
+foo             ; symbol — a variable reference
+ +

Keywords like :foo are a Clojure concept. In Squint, they + compile to plain JavaScript strings. This means :foo and + "foo" are interchangeable as map keys.

+ + + +

Functions

+ +

Define named functions with defn:

+ +
(defn greet [name]
+  (str "Hello, " name "!"))
+
+(println (greet "Alice"))
+;; Hello, Alice!
+ +

Conceptually, this produces JavaScript like:

+ +
// Simplified — actual output uses template literals
+var greet = function(name) {
+  return `Hello, ${name}!`;
+};
+
+squint_core.println(greet("Alice"));
+ +

Anonymous functions use fn:

+ +
(def add (fn [a b] (+ a b)))
+(println (add 2 3))
+;; 5
+ +

Short anonymous functions use #(...):

+ +
(def double #(* 2 %))
+(println (double 21))
+;; 42
+ +

Functions with multiple arities:

+ +
(defn greet
+  ([name]
+   (greet name "Hi"))
+  ([name greeting]
+   (str greeting ", " name "!")))
+ +

Variadic functions with &:

+ +
(defn log [level & msgs]
+  (println (str "[" level "]") (apply str msgs)))
+ + + +

Let bindings

+ +

Use let to create local bindings:

+ +
(let [x 10
+      y 20
+      sum (+ x y)]
+  (println "Sum:" sum))
+;; Sum: 30
+ +

Bindings are sequential — later bindings can reference earlier ones. + The let form compiles directly to const + declarations in JavaScript.

+ + + +

Data structures

+ +

Important: Squint's data structures are JavaScript objects and + arrays under the hood. The standard library functions like + assoc and conj return shallow copies + (not persistent data structures). Use bang variants + (assoc!, conj!) when you want in-place + mutation. See the Rationale for why.

+ +

Vectors compile to JavaScript arrays:

+ +
(def nums [1 2 3])
+(println (nth nums 0))   ; 1
+(println (count nums))    ; 3
+(println (conj nums 4))   ; [1, 2, 3, 4] — returns a new array
+ +

Maps compile to JavaScript objects:

+ +
(def person {:name "Alice" :age 30})
+(println (:name person))  ; "Alice"
+(println (get person :age))  ; 30
+ +

Sets compile to JavaScript Set objects:

+ +
(def unique #{1 2 3})
+(println (contains? unique 2))  ; true
+ +

The standard library functions (map, filter, + reduce, assoc, get, etc.) work on + these structures just as you'd expect from Clojure, but remember they + operate on mutable JavaScript types under the hood.

+ + + +

Destructuring

+ +

Squint supports Clojure's full destructuring syntax in let, + fn, and defn:

+ +

Sequential destructuring (arrays):

+ +
(let [[a b c] [1 2 3]]
+  (println a b c))
+;; 1 2 3
+ +

Associative destructuring (objects):

+ +
(defn greet-user [{:keys [name role]}]
+  (str "Hello " name ", you are a " role))
+
+(greet-user {:name "Bob" :role "admin"})
+;; "Hello Bob, you are a admin"
+ +

Under the hood, the compiled output uses squint_core.get() + to extract keys from the object — this preserves Clojure's keyword + lookup semantics rather than using JavaScript's native destructuring:

+ +
// Actual compiled output (simplified)
+var greet_user = function(p) {
+  const name = squint_core.get(p, "name");
+  const role = squint_core.get(p, "role");
+  return `Hello ${name}, you are a ${role}`;
+};
+ + + +

Control flow

+ +
;; if — returns a value (it's an expression)
+(if (> x 0)
+  "positive"
+  "non-positive")
+
+;; when — like if but no else branch
+(when (valid? input)
+  (process input))
+
+;; cond — multi-branch conditional
+(cond
+  (< n 0)  "negative"
+  (= n 0)  "zero"
+  :else    "positive")
+
+;; case — constant matching
+(case direction
+  "north" [0 1]
+  "south" [0 -1]
+  "east"  [1 0]
+  "west"  [-1 0])
+ +

Looping uses loop/recur (compiles to a + while loop) or doseq/dotimes:

+ +
(loop [i 0]
+  (when (< i 5)
+    (println i)
+    (recur (inc i))))
+
+;; doseq — iterate over a collection
+(doseq [x [1 2 3]]
+  (println x))
+ + + +

Namespaces and requires

+ +

Every Squint file is a namespace. The ns form at the top + declares it and its dependencies:

+ +
(ns my-app.core
+  (:require ["fs" :as fs]
+            ["path" :refer [join resolve]]
+            [my-app.utils :as utils]))
+ +

This compiles to ES module imports:

+ +
import * as squint_core from "squint-cljs/core.js";
+import * as fs from "fs";
+import { join, resolve } from "path";
+import * as utils from "./my_app/utils.mjs";
+ +

Notice: string requires ("fs") import npm/Node packages. + Symbol requires (my-app.utils) import other Squint namespaces.

+ + + +

JavaScript interop

+ +

Since Squint compiles to plain JS, interop is seamless:

+ +
;; Access JS globals with js/
+(js/console.log "hello")
+(def now (js/Date.))
+
+;; Property access
+(.-length "hello")       ; 5
+(.-protocol location)    ; "https:"
+
+;; Method calls
+(.toUpperCase "hello")   ; "HELLO"
+(.slice [1 2 3] 1)      ; [2, 3]
+
+;; Construct objects
+(def url (js/URL. "https://example.com"))
+
+;; Object literals with JS keys
+#js {:a 1 :b 2}
+ + + +

Async and await

+ +

Mark functions as async with ^:async metadata, and use + js-await to await promises:

+ +
(defn ^:async fetch-data [url]
+  (let [response (js-await (js/fetch url))
+        data     (js-await (.json response))]
+    (println "Got:" data)
+    data))
+ +

This compiles to native async/await + (actual output shown):

+ +
var fetch_data = async function(url) {
+  const response = (await fetch(url));
+  const data = (await response.json());
+  squint_core.println("Got:", data);
+  return data;
+}
+ + + +

JSX

+ +

Squint supports JSX via the #jsx reader tag:

+ +
(ns app
+  (:require ["react" :as react]))
+
+(defn App []
+  (let [[count setCount] (react/useState 0)]
+    #jsx [:div
+      [:h1 "Count: " count]
+      [:button {:onClick #(setCount inc)}
+        "Increment"]]))
+ +

The #jsx tag converts hiccup-style vectors into JSX + that works directly with React and other JSX-consuming libraries.

+ +
+ +

This covers the basics. For a complete reference of all built-in + forms, see the API Listing. For Clojure developers, + the Squint from Clojure guide covers + important differences.

+
+ + + +