Compile ClojureScript in Clojure into a String

I am learning to write simple web applications. I started with Clojure, Ring and Hiccup. I am progressing, so I believe I am not on a wrong path.

Now I am at the point where I want to integrate some JavaScript code into the html pages: Simple things like mouse events, changeing colors and requesting more contents. The JavaScript code in Hiccup are strings. And this is what I am not very happy about.

I always thought, that the goal of ClojureScript is to use the same language on the server and on the backend. But after reading tutorials and trying to become a friend of ClojureScript I have the impression, that ClojureScript cannot be mixed with Clojure.

All tutorials I can find generate the html pages on the client and use the server only for delivering pure data. I believe, that would work well and the frameworks would make it as easy as possible. But I don’t plan to develop such big web plattforms, where the huge effort for learning all these technologies pays off. I only want to create simple web applications, that come ready as html files out of the server with a bit of JavaScript integrated.

Do I misunderstand ClojureScript? Shouldn’t ClojureScript code be part of the Clojure Hiccup machinery on the server and converted to JavaScript during compilation?

1 Like

It seems maybe yes.

The typical arrangement is to use the clojurescript compiler (often w/ a tool such as shadow-cljs, or figwheel, or lein cljsbuild) to compile your clojurescript to javascript ahead of time, and then load the clojurescript-compiled-as-javascript files into your pages.

If you have code that you want to compile for both the jvm, and for js, it is possible to use .cljc files.

None of this necessitates any heavy frameworks. You can compile a small amount of clojurescript into a small amount of javascript, and load it like you would any other javascript.

Hope that helps.

I have the impression, that ClojureScript cannot be mixed with Clojure.

That’s true, although there’s a big overlap between what each supports. If a file can be run as both ClojureScript and Clojure, it’s usually given a filename which ends in .cljc.

But I don’t plan to develop such big web plattforms, where the huge effort for learning all these technologies pays off. I only want to create simple web applications, that come ready as html files out of the server with a bit of JavaScript integrated.

You’re not the only one to want that. Cherry and Squint, both by the redoubtably productive @borkdude, are very promising solutions for your use-case. They enable you to turn ClojureScript-like code into JavaScript without relying on the relatively heavy Google Closure library.

I don’t plan to develop such big web plattforms, where the huge effort for learning all these technologies pays off

Pretty much all of the tools that are needed to compile CLJS into JS have some sensible defaults that make working with small applications as easy as creating a config file with barely any data in it (and even this step is optional) and calling a single CLI command.

I only want to create simple web applications, that come ready as html files out of the server with a bit of JavaScript integrated.

Thing is, compiling CLJS into JS is not a trivial task, conceptually. It’s also slow, so if you want to embed CLJS code inside Hiccup forms, you’d have to cache the data, which already makes it all much less trivial than calling a single command to compile it all in advance.

And, while CLJS and CLJ are very similar, they have enough differences where some form can be valid in CLJS but invalid in CLJ (e.g. the #queue reader immediately comes to mind).

If you only want to create a simple web application, then I would recommend to consider using HTMX. Only reach for ClojureScript if you need to design a very interactive UI (like an image editor for example).

I recently wrote a blog post series on different strategies for using CLJS.

The idea of embedding CLJS directly into hiccup may seem appealing, but will not scale well. It didn’t work with JS and it wouldn’t be any better CLJS. Instead you want to make functions available in the client, that you can re-use in different places, with strategies such as “grafting” to “call” them from the server generated hiccup.

There is absolutely nothing heavy about the Closure Library, unless you use like 90% of it. CLJS itself uses less than 1% so it is never a worry that this is too heavy. What is heavy is cljs.core itself, since it has to provide all the persistent datastructures and core library. It optimizes decently well, but there is always a baseline cost associated with it.

I believe it always pays off to understand what you are working with, but if you wish to skip most of it you can maybe get by with learning the often discussed HTMX library instead. I’d strongly encourage to learn CLJS and a bit of DOM instead though, even if the learning curve is substantially steeper at first. It’ll be worth it in the end, and if you already know CLJ the CLJS part isn’t that hard.

I thought I’d do some small experiments as it’s been a while since I used ClojureScript. Here’s what I found: a ClojureScript ‘hello world’ program currently produces about 5MiB of JavaScript; with dead-code elimination that goes down to 96KiB right now. That’s not a disaster, but it’s considerably more than what you’ll get when using something like Squint: less than 1KiB after passing through esbuild. (Cherry actually produces three times as much code as ordinary ClojureScript though, so I’m not sure why the esbuild tree-shaking doesn’t seem to catch that).

That is about the baseline I was refering to. It can vary a bit depending on how much of cljs.core you use, but this is about to be expected.

Absolutely, but you are comparing apples vs oranges. From the squint README:

  • The CLJS standard library is replaced with "squint-cljs/core.js", a smaller re-implemented subset
  • Keywords are translated into strings
  • Maps, sequences and vectors are represented as mutable objects and arrays

There is more missing, but thats about already covers like 75% of what you get in cljs.core. Does it work? Sure. Is it the same thing? Absolutely not.

shadow-cljs can generate detailed build reports, if you are interested in what contributes to your build size.

1 Like

Indeed; they’re not directly comparable. However, it sounds to me like @habruening wanted something like a very thin layer over JavaScript to provide a functional programming environment with s-expressions. That’s certainly what I’m often looking for, so of course I might be over-projecting my own interests here.

I feel that that the relationship between ClojureScript and JavaScript is quite like the relationship between Pascal and x86 machine code: as a ClojureScript programmer, you hardly ever need to think about what JavaScript is being generated unless you’re creating a wrapper for an existing JavaScript library. Likewise, Pascal gives you this cozy environment where the CPU architecture is almost entirely abstracted away.

The relationship between Squint and JavaScript feels more like embedded C and machine code to me. There’s still loads of abstraction, but when you’re pushing bytes into buffers by their addresses and multiplying pointer widths together, it’s hard to forget that what platform you’re writing for. With a good disassembler and -Og in your CFLAGS, you can directly see what machine code a given portion of C produces.

Thanks for letting me know; that looks really useful :smiley:

Thank you to all for the detailed answers! This was very helpful. I was searching a very light solution. The solutions like squint are still too heavy. My html files have only a very few lines of JavaScript. Mostly one-liners that set a variable to something or call something.

I decided now to write my own Clojure syntax for JavaScript, which is converted to a string by functions and a macro. In case someone is interested, I paste here my draft.

(defn js+ [a b]
  (concat ["("] a ["+"] b [")"]))

(defn js- [a b]
  (concat ["("] a ["-"] b [")"]))

(defn jsdef [from to]
  (concat from ["="] to [";"]))

(defn jsdefn [name arguments & body]
  (concat ["function "] name ["("] [(clojure.string/join "," arguments)] ["){"] (apply concat body) ["}"]))

(defn jscall [name & arguments]
  (concat name ["("] (clojure.string/join "," (apply concat arguments)) [")"]))

(defn into-js [code]
  (cond (number? code) [(str code)]
        (symbol? code) [(str code)]
        (vector? code) (apply concat (map into-js code))
        (list? code) (if (= (first code) 'clj)
                       [(second code)]
                       (let [js-operation (->> code first (str "js") symbol resolve)]
                         (apply js-operation (map into-js (rest code)))))
        :else :error))

(defmacro js [& code]
  (let [translate-statement (fn tranlate-statement [statement]
                              `(str ~@(into-js statement)))]
    `(str ~@(map translate-statement code))))

;; Example

(def b 15)

(js (defn my_func [x y]
      (def tmp (+ x (clj b)))
      (def store tmp))
    (call my_func 3 4))
; => "function my_func(x,y){tmp=(x+15);store=tmp;}my_func(3,4)"