clojurescript

opqdonut 2026-03-20T11:28:13.304929Z

I'm hunting down a mystery related to constructor literals (record literals, #ns.Record{:field val}). I've had these work in a project, but now that I'm trying to make a minimal repro, they don't seem to be compiling in CLJS. See https://github.com/opqdonut/cljs-record-literal-repro . I'm probably missing something obvious...

opqdonut 2026-03-20T12:09:36.074049Z

That was probably a bit too opaque, let me inline the relevant stuff here:

(ns record
  #?(:cljs (:require-macros record)))

(defrecord Foo [field])
(ns repro
  (:require [record])
  #?(:cljs (:require-macros record)))

(prn #record.Foo{:field 1})
This fails to compile in CLJS with ClassNotFoundException, but works in CLJ. I've tried various permutations of :require-macros, no effect.

thheller 2026-03-20T12:32:38.434269Z

FWIW the reason this is complicated and CLJS and probably shouldnt be used is that the reader+compiler is not part of the runtime

thheller 2026-03-20T12:34:33.804299Z

meaning the reading happens at compile time and yields the CLJ instance of an object. the compiler then needs to know how to turn that CLJ/JVM object into JS code. which is ... not what happens for CLJ

☝️ 1
thheller 2026-03-20T12:35:42.809199Z

even for CLJ though it often falls back to use read-eval, which also kinda sucks 😛 talking about a AOT context

weavejester 2026-03-20T22:52:27.385499Z

I'm having some problems setting up a browser REPL in ClojureScript. I have a build with an :output-dir of "target/cljs/js" and this all works fine. However, the REPL doesn't seem to behave. When passed the same options as the build, it creates a ./out directory, despite this not being in the configuration. Why? It also sets the script locations as target/cljs/js which return 404s instead of the actual files. I'm really struggling in my attempts to get a basic build+REPL setup via the API.

weavejester 2026-03-20T23:12:58.411869Z

I've checked the repl-env argument and that has no ./out directory in any of its options, but for some reason the REPL feels the urge to rebuild to that path and then ignore it. I'm currently looking through the source to try and figure out why it's behaving this way.

weavejester 2026-03-20T23:27:26.556699Z

The default REPL appears to assume that you're serving from the local directory, and that the asset path is equal to the output directory. I'm unsure why this assumption would be made, as it's almost never true. For whatever reason it's also rebuilding everything unprompted and placing it in an out directory, which isn't defined in the REPL environment map or the option map.

weavejester 2026-03-21T00:37:36.595089Z

The more I look into the browser REPL included in ClojureScript, the more it looks like a prototype, or something experimental? It appears to be very brittle and only works under particular build configurations.

☝️ 1
thheller 2026-03-22T07:22:39.087189Z

FWIW the REPL stuff is always going to be a bit more brittle than clojure. in clojure the runtime can't disappear randomly from the REPL. in CLJS it can since you can just reload the browser at any point. so your R.PL parts of the REPL stay alive but the E disappears and loses all state and just reappears

thheller 2026-03-22T07:23:08.738269Z

thats why it works completely differently in shadow-cljs as well 😉

weavejester 2026-03-22T18:46:39.424219Z

I meant more in terms of how the cljs REPL expects that the asset-path is the same as the output directory. And how it uses its own custom HTTP server.

2026-03-23T06:48:26.314889Z

@weavejester I had the same experience. At some point, I spend some time trying to figure out do I really need Figwheel/shadow? Could I do ClojureScript just with the built-in browser REPL. I spend quite a lot of time reading the source and trying to figure out the exact right configurations, and if I remember right, I got it somewhat working, but it still felt brittle, the connection dropped quite often etc. So yeah, it felt a bit more like an experimental think or something you'd use just for the "getting started part" before switching to Figwheel/shadow.

2026-03-23T06:50:58.049239Z

I'm sorry I can't answer to your question (it's been too long since I tried this) but if it help, here's what I ended up having in my compiler options:

:output-to       "resources/public/js/compiled/app.js"
         :output-dir      "resources/public/js/compiled/app/out"
         :asset-path      "/js/compiled/app/out"
(and yes, as you pointed out, asset-path equals to output dir) And then in browser-repl options I added:
{:port 3449

         ;; Let's keep this false. To my experience, if this is set to true,
         ;; it's common to see "Broken pipe" socket error.
         :launch-browser false

         :static-dir [;; The index.html file that is served on localhost:<port>
                      ;; by the Browser REPL static file server is located in resources/public
                      "resources/public"

                      ;; Add output-dir here so that the Browser REPL static
                      ;; file server can find and serve the compiled files.
                      (:output-dir compiler-options)]}

Leaf Garland 2026-03-23T09:58:59.897279Z

The unprompted build to out sounds like you are running the repl without giving it your compile options, so it is using the defaults. Note the --compile-opts flag in this alias. Similar options can also be given to cljs.repl/repl if you are starting the repl from code.

:aliases
 {:repl
  {:main-opts ["-m" "cljs.main"
               "--compile-opts" "cljs-opts.edn"
               "--repl-opts" "{:launch-browser false}"
               "--repl"]}}
cljs-opts.edn:
{:output-to "resources/public/js/main.js"
 :output-dir "resources/public/js/out"
 :asset-path "js/out"
 :main }
For what it's worth I've not had any issues with cljs browser repls but it did take a while (and some reading) to figure out.

👍 1
dnolen 2026-03-23T13:49:04.314929Z

I only use Browser REPL, it's definitely a bit basic, it could be improved but I don't think it gets enough use to get decent reports. It works well enough for me.

dnolen 2026-03-23T13:50:16.077359Z

I also only use the built in Node.js REPL and (previously) Krell (React Native) was based on the existing REPL stuff.

dnolen 2026-03-23T13:55:24.392759Z

so I wouldn't say experimental - just probably lacking some care 🙂

👍 1
dnolen 2026-03-23T13:56:06.056339Z

Always keen on getting patches and ideas here!

dnolen 2026-03-23T13:57:39.509849Z

That said CLJS REPLs aren't very simple - it's easily the most complicated thing in the entire code base besides the compiler and analyzer.

weavejester 2026-03-23T14:05:36.243069Z

I haven't looked through the CLJS REPL in great detail, but one thing I did notice is that it handles the asset path oddly. My assumption would be that the relative path of the file to the output-dir would first be computed, and then the asset path added to the relative path. For example, if my output-dir is target/cljs/js and I have an asset target/cljs/js/main.js, then first I'd assume it calculates the relative path - main.js - and then adds the asset-path /js to the front separating by a forward slash if needed: /js/main.js. Instead, the CLJS REPL appears to assume that the asset-path is always identical to the output-dir. So if the output-dir is target/cljs/js then the path of the asset is assumed to be /target/cljs/js/main.js. Unless the output-dir is absolute, for example /tmp/cljs/js, in which case the path of the asset is /main.js.

dnolen 2026-03-23T14:09:09.345889Z

hrm, pretty sure :asset-path is absolute path for web servers.

weavejester 2026-03-23T14:09:37.675639Z

Is the CLJS REPL in ClojureScript complex just because it needs to create its own web server? I (perhaps naively!) thought it looked relatively straightforward otherwise: compile CLJS to javascript using a compiler environment that persists through the loop, send to browser, browser evaluates javascript and returns the result (if printable). Am I missing something important?

dnolen 2026-03-23T14:09:51.178289Z

i.e. need to resolve source maps etc.

dnolen 2026-03-23T14:10:37.295239Z

right, it's important to not conflate CLJS REPL and browser REPL here.

dnolen 2026-03-23T14:10:55.868079Z

let's focus on why the REPL is complex

dnolen 2026-03-23T14:11:16.442519Z

problem number one is that the JS runtime is remote

dnolen 2026-03-23T14:11:31.502709Z

even Node.js is a socket based thing

dnolen 2026-03-23T14:11:51.744679Z

resolution of files and loading of files is different across all JS targets

dnolen 2026-03-23T14:12:51.732279Z

we must bring our own resolution mechanism on top of every target, which is a combination of GClosure conventions and classpath

dnolen 2026-03-23T14:13:58.541909Z

the browser JS runtime is transient (the user can refresh it)

dnolen 2026-03-23T14:14:29.477339Z

so now we need both reconnect logic, but also we need analysis paths so everything doesn't show up undefined

dnolen 2026-03-23T14:14:47.136479Z

this is just the initial set of hard problems 🙂

dnolen 2026-03-23T14:17:19.026919Z

note that cljs.repl is the core functionality

dnolen 2026-03-23T14:17:37.480019Z

Node.js, Browser, third party stuff like Krell use extension points.

dnolen 2026-03-23T14:17:51.440169Z

In the old days we had Nashorn and Rhino as well.

dnolen 2026-03-23T14:18:44.303589Z

the whole thing is sufficiently complicated that Figwheel and Shadow both reimplemented a lot of stuff.

dnolen 2026-03-23T14:19:03.801689Z

I don't know if they support all the Clojure flags like CLJS attempts to

weavejester 2026-03-23T14:19:10.784499Z

Thanks for the explanation! I'm currently only interested in a browser REPL, which at least simplifies the target. My current plan is to write my own websocket based REPL server/client, since I don't have the same restrictions that ClojureScript itself has (i.e. I can use external libraries). It may turn out to be that I write a repl-env that I can slot into cljs.repl, though I also want compatibility with rebel-readline.

weavejester 2026-03-23T14:20:05.832719Z

I've worked with both Figwheel and Shadow in the past, but they're very much designed as tools first - it was hard to use them as libraries.

dnolen 2026-03-23T14:21:42.230639Z

browser REPL does a bunch of stuff to make it "easy" for first time users and to avoid any web server deps

dnolen 2026-03-23T14:21:52.155759Z

it's not a great starting point

dnolen 2026-03-23T14:22:32.947229Z

whether your interested in React Native or not one goal w/ Krell was to show how to use what's in CLJS as a library

dnolen 2026-03-23T14:22:34.300949Z

https://github.com/vouch-opensource/krell

dnolen 2026-03-23T14:24:56.095539Z

there's still a bit to digest here, but I think I had a primitive thing working in a week or so.

weavejester 2026-03-23T14:25:04.016069Z

Thanks! That looks like a pretty useful reference along with cljs.repl.browser.

dnolen 2026-03-23T14:25:47.632259Z

so the key thing is you can skip all the complexity if you start w/ eval

dnolen 2026-03-23T14:26:36.300779Z

that's how I did all of the existing ones, and I think stubbed out file loading etc.

dnolen 2026-03-23T14:26:48.470229Z

once you can eval (+ 3 4)

dnolen 2026-03-23T14:26:55.526559Z

next step is "bootstrapping"

dnolen 2026-03-23T14:27:08.547379Z

which is enough code to load deps

dnolen 2026-03-23T14:27:26.839819Z

and then the rest becomes more clear

dnolen 2026-03-23T14:27:47.088039Z

feel free to ask questions in #cljs-dev is you need more assistance

weavejester 2026-03-23T14:29:55.447009Z

Would bootstrapping be required if there's already files from cljs.build.api? i.e. if I build the ClojureScript files first, with the REPL server as a preload, then the browser should already have all the JS necessary to goog.require etc. right? Or is that naive?

weavejester 2026-03-23T14:30:40.783369Z

Also thanks for the #cljs-dev tip! I'll try not to ask too many questions - I really appreciate you taking time to answer.

thheller 2026-03-23T14:30:45.896559Z

goog.require doesn't actually do anything at runtime. goog.require/provide are essentially metadata for the compiler

dnolen 2026-03-23T14:31:19.131499Z

you still need to bootstrap because goog.require etc. don't understand reloading namespadces

dnolen 2026-03-23T14:31:30.819259Z

so no build stuff is not sufficient

dnolen 2026-03-23T14:31:44.467829Z

browser REPL does this hack

dnolen 2026-03-23T14:34:25.470349Z

but also you could do it differently, Shadow doesn't use the debug loader for example

weavejester 2026-03-23T14:34:47.140869Z

Ah, I see. Presumably I can just use the ClojureScript bootstrapper to patch goog.require?

dnolen 2026-03-23T14:35:38.516599Z

that should work

weavejester 2026-03-23T14:37:02.449399Z

Excellent. Again, hugely appreciate everyone taking their time to educate me on this.