cljs-dev

weavejester 2026-04-06T13:08:17.131569Z

I notice that with optimizations :none, the ClojureScript build API adds script elements to the document body to load up the various individual scripts needed. Conversely, Shadow CLJS uses an inline eval and the sourceURL pragma. I assume the main advantage of Shadow's approach is that it avoids the DOM being cluttered up with a few hundred script tags, but are there any other reasons to prefer one approach over the other?

Adam Kalisz 2026-05-11T14:49:49.653029Z

What is the effect on Content Security Policy? It seems inline scripts are a bit frowned upon: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src

thheller 2026-05-11T14:55:40.880059Z

this is all about development builds where CSP generally isn't a concern

👍 1
Adam Kalisz 2026-05-11T19:18:16.496769Z

It may be, to mimic some of the aspects of secure sites as early as possible. We did a lot of stuff like that with HTTPS in dev, CSP etc.... we did have an (if dev ...) guard for that. Thanks for the clarification though.

thheller 2026-05-12T05:48:53.539819Z

I agree that dev should be as close as possible to production, but for CSP we need eval and other "insecure" things for hot-reload and the REPL to work. so, that is something that has to be different. Unless you do not need that 😉

👍 1
thheller 2026-04-06T15:43:15.302569Z

performance is pretty much the only reason. shadow-cljs can also still load stuff via script tags, its just substantially slower

thheller 2026-04-06T15:43:53.273069Z

:loader-mode :eval described in that blog post has been the default for 7+ years now

thheller 2026-04-06T15:45:19.572229Z

loading via script tags is the default "debug loader" mechanism inherited from the closure library

weavejester 2026-04-06T21:21:55.249239Z

Oh, interesting! Thank you very much for the explanation. Does Shadow generate its own "main" javascript file when optimizations are :none, then?

weavejester 2026-04-06T22:51:49.317989Z

Ah yes, it does: shadow.build.targets.browser/generate-eval-js-output

thheller 2026-04-07T04:26:34.158649Z

yes, shadow-cljs generates all its own outputs. during compilation everything goes into memory into the build state and writes nothing to disk besides caches. then there is an extra "flush" step where each :target impl decides where and to put what. in case of browser builds the default is the eval stuff and the extra cljs-runtime dir for source maps etc

weavejester 2026-04-07T13:54:13.073119Z

Interesting! I feel like I'm learning a lot more about Shadow now that I'm working with build.api, and also gaining a new appreciation of how much Shadow does behind the scenes.

😎 1
weavejester 2026-04-07T21:53:02.391309Z

Presumably hot-reloading works in the same fashion? That is, Shadow passes the updated source code received from the server to a global eval with a sourceURL pragma?

thheller 2026-04-08T05:00:18.783069Z

kind of yeah. the REPL implementation in shadow-cljs works completely differently than the cljs.* stuff. there is a compiler-side part that takes user input and turns it into actions. the actions are then transferred to the runtime and interpreted there. the default repl-compile produces the source map and ships that as part of the "action". https://github.com/thheller/shadow-cljs/blob/eabf1592cbc34b25f8d393c2a66e5d937ffa2c40/src/main/shadow/cljs/repl.clj#L555-L594

thheller 2026-04-08T05:00:24.190079Z

hot-reload essentially works the same way

thheller 2026-04-08T05:03:57.660569Z

hmm no, remembered that wrong 😛 I run the normal compile there and that generates the source maps anyway, so loading those via sourceURL. https://github.com/thheller/shadow-cljs/blob/eabf1592cbc34b25f8d393c2a66e5d937ffa2c40/src/main/shadow/cljs/devtools/client/browser.cljs#L23-L40

thheller 2026-04-08T05:05:00.976219Z

I tinkered with a "simplified" hot-reload that basically just does a (load-file ...) to basically re-use the REPL stuff, but I never actually finished that

thheller 2026-04-08T05:06:14.020009Z

shadow-cljs generates a build-complete websocket message with some info about which files have been recompiled. the client decides what files it is interested in and then loads and evals them

weavejester 2026-04-08T11:23:31.778989Z

Thanks for the explanation, that's really helpful. I was thinking of handling hot reloading in a similar fashion and was wondering if there were any pitfalls I hadn't thought of. Currently I'm using the bootstrapping from ClojureScript and then running (require 'foo :reload) , which ultimately uses document.write to add script elements to the document. But I could instead transfer the updated JS via websocket and eval it with a sourceURL, or alternatively use an XHR to grab it and just deliver the URI via websocket.

thheller 2026-04-08T11:31:20.950259Z

document.write you can only do while the page is still loading, so doesn't work for hot reload

thheller 2026-04-08T11:31:58.515349Z

at least I thought so 😛 shadow-cljs otherwise uses fetch to load scripts and evals them. or websockets, depends a little on which environment things run in

👀 1
thheller 2026-04-08T11:32:42.892309Z

node just reads files directly from the fs

weavejester 2026-04-08T11:33:27.779529Z

Oh, the cljs bootstrapping actually uses document.body.appendChild instead, now that I check: https://github.com/clojure/clojurescript/blob/6c7a1680b29e9215e95d867c89f54f2107806220/src/main/cljs/clojure/browser/repl.cljs#L186C13-L186C24

thheller 2026-04-08T11:34:00.304889Z

yeah I never want to clutter the DOM with a bunch of extra script tags, so eval is better I think

thheller 2026-04-08T11:34:34.113669Z

also otherwise breaks when using <script defer src=...> since that can't document.write at all

weavejester 2026-04-08T11:36:00.348419Z

That was my thought as well. I've got hot-reloading working with the default cljs stuff, but my thought is that I can omit :output-to, write my own load script, and use a global eval to reload the scripts, which would ensure I don't need to write any extra script tags.