shadow-cljs

Marten Sytema 2025-05-13T06:48:53.340489Z

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}

thheller 2025-05-13T06:57:40.783419Z

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

thheller 2025-05-13T06:58:31.543829Z

won't hurt anything, so safe to ignore

Marten Sytema 2025-05-13T06:58:36.613669Z

thx!

thheller 2025-05-13T06:58:49.185739Z

https://github.com/den1k/re-frame-utils/issues/5

thheller 2025-05-13T06:59:36.910219Z

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

Roman Liutikov 2025-05-13T10:08:50.986519Z

@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.

thheller 2025-05-13T11:24:02.109449Z

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

thheller 2025-05-13T11:24:53.134489Z

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

thheller 2025-05-13T11:25:09.487329Z

basic graph theory stuff

thheller 2025-05-13T11:25:22.353319Z

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

thheller 2025-05-13T11:34:18.110249Z

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

thheller 2025-05-13T11:36:48.914749Z

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

thheller 2025-05-13T11:39:23.089619Z

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

thheller 2025-05-13T11:40:20.318829Z

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

thheller 2025-05-13T11:41:15.519999Z

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)

thheller 2025-05-13T11:41:50.942439Z

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

thheller 2025-05-13T11:42:34.381929Z

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

thheller 2025-05-13T11:43:42.440199Z

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

thheller 2025-05-13T11:44:21.504149Z

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

thheller 2025-05-13T11:44:36.068789Z

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

Roman Liutikov 2025-05-13T11:50:49.366079Z

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

Roman Liutikov 2025-05-13T11:51:26.702709Z

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

thheller 2025-05-13T11:53:11.226219Z

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

πŸ‘ 2
thheller 2025-05-13T11:55:59.550279Z

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

thheller 2025-05-13T11:56:46.566409Z

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

thheller 2025-05-13T11:57:51.837059Z

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

πŸ‘ 1
thheller 2025-05-13T12:38:46.633279Z

(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
  )

thheller 2025-05-13T12:39:20.489639Z

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

thheller 2025-05-13T12:40:20.805519Z

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

thheller 2025-05-13T12:40:58.747729Z

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

thheller 2025-05-13T12:41:32.446059Z

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

thheller 2025-05-13T12:43:11.127379Z

configure is how build configs are normally processed using the :target stuff https://github.com/thheller/shadow-cljs/blob/084b63cdfbcbc72e52f573f60064e6acc3848700/src/main/shadow/build.clj#L314-L442

thheller 2025-05-13T12:43:56.991059Z

optimize for closure, etc

thheller 2025-05-13T12:44:40.354759Z

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

thheller 2025-05-13T12:45:11.890079Z

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

thheller 2025-05-13T12:45:24.978239Z

dunno if that makes sense πŸ˜›

thheller 2025-05-13T12:48:02.523199Z

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)

thheller 2025-05-13T12:48:44.472559Z

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

Roman Liutikov 2025-05-13T12:50:59.554169Z

nice! all of this should be helpful, thank you

thheller 2025-05-13T12:53:05.013079Z

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
thheller 2025-05-13T12:53:19.755489Z

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

thheller 2025-05-13T12:54:16.038849Z

basically how webpack does it