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?
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
this is all about development builds where CSP generally isn't a concern
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.
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 😉
https://clojureverse.org/t/improving-initial-load-time-for-browser-builds-during-development/2518
performance is pretty much the only reason. shadow-cljs can also still load stuff via script tags, its just substantially slower
:loader-mode :eval described in that blog post has been the default for 7+ years now
loading via script tags is the default "debug loader" mechanism inherited from the closure library
Oh, interesting! Thank you very much for the explanation. Does Shadow generate its own "main" javascript file when optimizations are :none, then?
Ah yes, it does: shadow.build.targets.browser/generate-eval-js-output
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
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.
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?
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
hot-reload essentially works the same way
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
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
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
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.
document.write you can only do while the page is still loading, so doesn't work for hot reload
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
node just reads files directly from the fs
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
yeah I never want to clutter the DOM with a bunch of extra script tags, so eval is better I think
also otherwise breaks when using <script defer src=...> since that can't document.write at all
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.