shadow-cljs 2025-05-13

Hi folks, i’ve upgraded to latest shadow-cljs, and thereby also clojure and clojurescript to the latest. I get this now on circleCI, I was wondering what that means

[2025-05-12 14:50:45.831 - WARNING] :shadow.build.classpath/bad-jar-contents - {:jar-file "/home/circleci/.m2/repository/re-frame-utils/re-frame-utils/0.1.0/re-frame-utils-0.1.0.jar", :bad-prefix "out", :bad-count 67}

it means the distributed jar contains contents it shouldn't contain, like compiled clojurescript core sources

won't hurt anything, so safe to ignore

(shadow-cljs now just ignores those, but long ago got confused)

@thheller have you ever looked into automatic code splitting in shadow? I want to explore it a bit more. From what I understand chunking is Closure's job, but looks like shadow is doing some preparations before feeding everything to Closure. I'm wondering what would be the easiest hacky way to prototype this.

I did experiment but couldn't find a solution I was happy with

as far as closure is concerned it needs a definition of chunks. as in which files are grouped together and chunks can depend on each other

basic graph theory stuff

how those chunks are defined and which files go where the closure compiler doesn't really care about

I never looked into how that works, but might be a useful reference?

I could never figure out decent heuristics about when something should be in its own chunk or moved to different chunks

A depends on B, C uses like a 5% bit of A. the naive way just makes C depends on A, which is not ideal. neither is moving that bit to B. neither is creating an extra chunk. it all depends on manual configuration is the only way I found to deal with this

code splitting will always require the user changing code, so it'll never be fully automatic

problem is if you split too much you end up with many tiny chunks that individually are much larger than together (compression gets better with more files)

splitting always introduces an async aspect as well, which makes things icky in the user code

so in the end I found the manual :modules definition the most reliable and adjustable. user is in full control, but in turn makes things a bit more complicated

you can experiment with this from within shadow-cljs with a custom :target I guess

or I could give you a snippet that just uses the lower level shadow.build things

and just use the build state to produce a :modules map I guess

yeah I know auto splitting adds complexity, afaik in js world small chunks are merged together, there’s a threshold

Custom target sounds good. Can you also send that snippet?

problem is you need to define the chunks before :advanced. so you don't really know the size its gonna be πŸ˜›

πŸ‘ 2

when I built the :npm-module target (one chunk per ns) I encountered some issues that basically disabled some :advanced things, so the end result was also larger than needed

:esm-files also has that issue, can even leave files completely empty because all the code is moved out of them

but I guess getting something equivalent to webpack shouldn't be that hard

πŸ‘ 1

(ns shadow.raw-build
  (:require
    [shadow.build.api :as build-api]
    [shadow.build.npm :as npm]
    [shadow.build.resolve :as res]
    [shadow.cljs.devtools.server.util :as util]))


(let [build-state
      (-> (util/new-build {:build-id :raw-test
                           :js-options {:js-provider :shadow}} :dev {})
          (build-api/with-npm (npm/start {})))

      [entries build-state]
      (res/resolve-entries build-state '[foo.bar])
      
      build-state
      (build-api/compile-sources build-state entries)]

  ;; in build state
  ;; :sources  holds all the info about the source files
  ;; :outputs  holds all the output data, nothing is written to disk yet (besides cache)
  ;; [:compiler-env :cljs.analyzer/namespaces] holds all compiler metadata
  ;; figure out :modules now
  )

basically after compiler-sources you'd have all the data available you could possible need to figure out :modules

resolve-entries basically expects a vector of namespace symbols that should be compiled (+ their dependencies). resolve is what orders them also

:target is basically a higher level of abstraction for all this, but is doing all the underlying work

watch basically just runs this in a loop -> resolve, compile, flush, wait

optimize for closure, etc

I think you'd need to compile one without modules to figure out what the sources actually do

then setup modules, maybe compile again, or just use the state that already has it compiled

dunno if that makes sense πŸ˜›

build-state is rather large. I built Inspect in the shadow-cljs to be able to look and browse it. so just (tap> build-state) in the snippet above (when run via shadow-cljs clj-repl or so)

printing it will for sure blow up your REPL πŸ˜› its like 200mb when printed

nice! all of this should be helpful, thank you

the thing I experimented with basically started with one entrypoint (regular ns), that or its dependencies would use a special macro which left some metadata about dynamic imports

πŸ‘ 1

so the thing would run in loop until no new things were found

basically how webpack does it