Fork me on GitHub

ClojureScript consumption of JS libraries has come a long way, and is really great now. It's very powerful to be able to depend on node packages, which is now possible without needing to rely on CLJSJS or similar thanks to Webpack integration and Externs Inference. I propose that ClojureScript lean in to extern inference by changing: 1. :infer-externs compiler option default to true 2. When no type hint is provided, create an extern anyway 3. Introduce *infer-externs* bound to true by default as a way to opt-out of (2) Motivation: The current situation is that it is challenging to create a reliable advanced or even simple optimization build. One must a. Make sure that you turn on externs inference or source externs from somewhere. b. Find and annotate lots of code like:

(.then (fn [^js/object response]
               (conn/transact-entities! "calendar" [(.-result response)])))
If I miss the ^js/object, my application will break unexpectedly at the worst time; deployment. I won't notice it in development. Tests wont catch it because tests can't be run with advanced builds. c. Practically it is impossible for me to find all the places where I need to add a ^js/object so I must call (set! *warn-on-infer* true) at the top of every ClojureScript file (if I forget, I'll probably be O.K. for a while until I do some interop in that file, then broken). This sets up a frustrating cycle where the compiler is essentially prompting me to scurry around adding ^js/object to places, but only if I ask it to let me know about them. I feel like I'm working for the compiler, not the other way around, and the result is ugly (concretion of noise in my codebase). d. In some rare circumstance it is better to use a different hint from ^js/object, but I'd much rather have the onus be on me to decide when that is necessary. What do you think?

👍 1

shadow-cljs has introduced the default :infer-externs :auto many many years ago for this. it basically sets *warn-on-infer* to true for your source files (defined as files not being in .jar files, aka libraries)


this works while supressing most noise. defaulting to true for everything gets very noisy for cases you can't do much about


instead of ^js/object just ^js is also fine


Maybe an interop encapsulation function could help to reduce the pain. This would also reduce the amount of places to be annotated.


Hi Thomas, I agree that shadow-cljs greatly improves over the default compiler experience, and in reality people who want an advanced build should use shadow instead. FWIW I'd love it even more if code like: (fn [x] (.-foo x)) needed no ^js annotation at all.


didn't mean to suggest that everyone should be using shadow-cljs. if someone wants to use behavior it has as a reference and try to make a patch for CLJS proper that might get accepted


(fn [x] (.-foo x)) the issue with this is that if it was the default to never rename anything unknown :advanced would perform much worse. I don't have any numbers and I would indeed prefer an option for that, just to make :advanced a little more forgiving. However that would likely need to happen in the closure compiler, not in CLJS.


I'm not talking about never renaming anything, I'm talking only about user written object access code 🙂 For example: (fn [x] (.-foo x)) name munging is never going to be correct unless x is known. If x is not typed, why name munge it? There are only a limited number of uses of .- in a user program, name munging only breaks the code. So users have to either be very mindful of this, or don't use .- use goog.object or aget or something else. It just seems like a very weird choice to me.


"why name munge it" is precisely the cause


if EVERY place in your code uses .-foo it is entirely safe to be renamed, regardless of knowing the type


the problem only arises if some places use alternate methods (eg. goog.object) or are not part of the renaming process (eg. npm deps)


so some places end up renamed while others are not


yes, the "safe" way is to never rename anything unknown but that also leads to :advanced being much worse

Shuky Badeer06:04:25

Hi guys! I'm trying to use the library cljs-time. So I'm including

[com.andrewmcveigh/cljs-time "0.5.2"] 
in the lein project and restarting repl. Now when i add it to the namespace
(ns growth-clojure-library.time.time
  (:use [cljs-time.core]))
It says
Execution error (FileNotFoundException) at growth-clojure-library.time.time/eval1553$loading (time.cljc:1).
Could not locate cljs_time/core__init.class, cljs_time/core.clj or cljs_time/core.cljc on classpath. Please check that namespaces with dashes use underscores in the Clojure file name.
Am i missing something here? Thank you!


Looks like you're trying to use a cljs library in a clj project. Maybe try clj-time? [clj-time "0.15.2"]

Shuky Badeer06:04:49

@U7RJTCH6J Basically I'm creating a library for both clojure and clojurescript. I'm already using clj-time to handle the clojure case and i wanna use cljs-time to handle the clojurescript case. Like there is a way to tell whether the library is being invoked in clojure project or a clojurescript project and so i make the choice between clj-time and cljs-time dynamically.


The short answer is reader conditionals,

Shuky Badeer06:04:36

Sure i'll dig into that. In the meantime I wanted to ask, how do i create and package a clojurescript library? Is there a guide for that?


That's a good question. I'm not sure what guides are available for deploying a clojurescript library. I'm also not sure if there's any special considerations for deploying a cljs or cljc library, but maybe someone else might have a good recommendation.


I think it's basically the same process as deploying a clj library,


It's basically renaming .clj to .cljc and fixing some interop/reader conditionals

👍 1

Other than that it's the same procedure.

👍 1
Shuky Badeer11:04:53

@U07FCNURX right i did that actually. But after realizing that it's strictly a front end library i switched things to clojurescript

Shuky Badeer11:04:44

Can i ask u guys about this error? First time i include dependencies in deps.edn format. Does anything look wrong in this photo?


wrap :dependencies in a vector - it is a vector of 2-tuples (vectors with 2 elements)

Shuky Badeer12:04:36

Roger that! Thank you!


@U033V0AJFU4 You likely want to use cljc-java-time.


Hi. Why the following code doe not return true ?

(instance? (type "") "")


Because that's how it works in JavaScript:

=> "" instanceof String


And, just in case - if you need to check whether something is a string, there's string?. But it will work only for primitive strings and not string objects, so it's pretty much the reverse of (instance? js/String ...).


How can I get the tyepof operator in cljs?


Yup. Right inside the string? function doc, I saw goog/typeof


With CLJS we usually require libraries / modules. I have the need to integrate with a couple of simple Class definitions is xyz.js ... how can I access that without making a fake library? [ I'll do that if I have to obviously ]


if it is Closure style JS it just works, if it isn't you need to do the usual external library song and dance

😬 1

I am using nbb and there I can just do this style


(:require ["signature$default" :as signature])
(new signature "API_KEY "API_SECRET")


no fuss ... except I had no idea WTF $default was until @U04V15CAJ mentioned it


Hahaha funny you bring that up, I learnt of $default yesterday watching a youtube where borkdude just casually meantioned it 🙂

❤️ 1

note that $ is a actually generic feature to pull apart JS libraries

chef_kiss 1

$default is only one case it handles


yes, $foo also works, it's just that default is a common name for the default exported value (and has special meaning within import syntax but CLJS isn't dealing with that part)


finding out how to call JS things is always a new adventure


@timothypratley I think you need to clarify some expectations. If you're sourcing stuff from node_modules for browser :target :bundle already does most of these things anyway


Hi David, Yes I am sourcing stuff from node_modules for browser, and no :target :bundle does not.


hrm the only thing it doesn't do as far as I can tell is #2?


fwiw, it would be good to hear more of what your problems are because this stuff is pretty old now and at least in the projects that I currently work on we haven't hit advanced compilation issues in the entire time and that's w/ devs who are not ClojureScript experts doing most of the lifting


said another way, the only thing it does is #1 😜


sure, I'm happy to share


Here's a sample of the "fallout effect":


hard to read, gist?


I mean looking at that quite a few things seem outright wrong which need explaining


Let's start with one 🙂


why are you hinting browser stuff when Google Closure provides externs for standard JS browser interfaces?


which are included by default


If you pick a specific one I can expand the context and explain why I needed to add the type hint.


any HTML element, the Event type etc. they all need explaining


Promise, etc. etc.


this all seems very strange


O.K. so a simple Event type example is:

[ui/button {:icon     (str "circle" (when circle " outline"))
                    :style    {:padding 0}
                    :size     "tiny"
                    :basic    true
                    :circular true
                    :on-click (fn [^js/Event ev]
                                (.stopPropagation ev)
                                (rm/record! context id {:circle (not circle)}))}]


how is that not simply wrong?


Without hinting it, the compiler will produce a warning.


What is your definition of "wrong"? The above code works in advanced mode.


I see because you are mixing stuff


That needs hinting and that doesn't need it


So warning generated


The issue is, if I don't set warn-on-infer, I'll definitely miss they cases where I must type hint


And those are harder to predict because I'll only see them in production if I regression test the app.


Or maybe I'm not understanding what you are saying. If I'm just doing it wrong, that would be grreat to know.


But why not separate, js requires are poisoned


Interop can (and does) happen anywhere, not just in namespaces that require js.


For example the local storage stuff


the interop is contained in a ns


but other things look at the things it produces.


I'm assuming you are implying "Why not only warn-on-infer in namespaces that require a node module" << which might not be what you mean.


"js requires are poisoned", really getting the hot takes now hahahaha 🙂


You don't need to warn most of the time


The calls are poisoned


So I want to see your cases where actually need the hint which lead you down this unfortunate path


Rather than incidental stuff that got tacked on


Is this a better bottom level example?


It wont work without type hinting.


so yucky 🙂


none of the Promise hints should be necessary


I agree completely.


exactly one hint is needed in this file




anyways - stepping back what I'm trying to suggest is this


all JS imports are poisoned


all direct calls and all direct return values should also be be poisoned (if something is missing we should fix that)


the only place where this falls down in typical interop are callbacks


so rather than doing what you're suggested I think it would be better to figure out how to do the right thing without setting warnings


we don't use infer warnings in our projects and advanced builds work


yeah in this file you could avoid all this by dropping the infer warnings


adding one type hint (modulo inference bugs)


er actually, using goog.object helper on result in the promise callback then no hints


what I'm trying to suggest is that you don't need warnings, that's what :bundle :target was designed to solve which came later and was informed by :advanced issues in practice


if you have to write a hint - something is wrong


it is possible we could better handle callbacks / promises coming from poisoned requires - but that needs thought


I appreciate that. Following your advice above does not work in practice for me because of the "something is wrong" part; For example doing exactly what you describe above results in: main.js:35235 Uncaught TypeError: $G__19485$jscomp$1$$.$useDeviceLanguage$ is not a function ^^ Usually not printed so nicely,, I have to add compiler options

:pseudo-names true
:pretty-print true


i.e.: oh the call (.useDeviceLanguage) broke


why? I don't know


this is a Firebase problem right?


If I type hint it, it works


oh you're just talking about your thought process


this case is simply a bug


we have so many tests for externs inference, and when people take the time report specific cases it's a huge help


FWIW I do try to report bugs, often I don't know what is a bug or not


If you tell me this is a bug, I believe you of course


but when I encountered it I thought it was because I didn't type hint it


I didn't recognize it as a bug.


not calling you out specifically sorry here


this would appear to catch your case


I think what is making this break is possibly the :refer [getAuth]


to clarify my earlier point which might have been hard to understand before


all functions coming from a NPM library is auto-poisoned w/ ^js hint


all returns from such a call are also poisoned


all subsequence method invokes or property access on that the poisoned value is also poisoned


it uses the type hinting machinery so the boundary is function arguments


function returns are also inferred


if you understand this then you can identify bugs


the externs inference test ns is a good primer on what is supported


(and what might be missing)


just for reference if I replace the :refer with :as and do fauth/getAuth is behaves the same (just tested)


should be trivial to write a test for this


if you're not doing that, then you're probably just writing Closure JS + ClojureScript and why do you care?

Charles Comstock15:04:58

I have a question about tap> forwarding to js/setTimeout. This maybe is abusing tap> but I thought it would be useful to have a with-tap function that would create an (atom []), add a tap fn to conj results onto the atom, execute a function (presumably with calls to tap>), remove the added tap, and return the results of each call to tap inside the function along with the original results of the function. I thought this might be a useful alternative over println in a repl environment so that the debug log & final result could be inspected as a single return value. However, since tap> is executing using js/setTimeout, it appears that the atom is not filled with values until after with-tap finishes. One idea I had was to rebind *exec-tap-fn* using a synchronous implementation, but was having difficulty doing that in a cljc file. If I just use a single global add-tap call and don't remove-tap after that will also work, but thought it might be helpful to limit the scope of the tap function. I suppose I could also write a replacement function for tap> that is a no-op if it's not wrapped in with-tap, but was hoping to re-use tap>

(defn with-tap [f]
  (let [tap-log (atom [])
        tap-f (fn [v] (swap! tap-log conj v))
        _ (add-tap tap-f)
        result (f)]
    (remove-tap tap-f)
    {:log @tap-log :result result}))

(comment (with-tap (fn [] (tap> 1) (tap> 2) 3))) ;; expecting => {:log [1 2] :result 3}, but instead getting {:log [] :result 3}
Has anyone else experimented with local, temporary tap handlers and had any luck, or is this just going against the grain?


Not sure if this is useful, but you could try something like:

(with-redefs [tap> println]
  (tap> ::hi))
especially because you only care about sync taps

Charles Comstock16:04:22

Thanks, I think just rebinding cljs.core/*exec-tap-fn* in a #?(:cljs) block to make it synchronous is good enough, though rebinding tap> as the swap command might also work. I explicitly do not want to use println though as that means the output is separated from the data.


Sorry, yeah the the println is a placeholder for user logic 👌


The main advantage of the with-redefs is that I think you will run into the same problem with tap> in clj :thinking_face:

Charles Comstock16:04:35

Yea I guess:

(defn with-tap-log [f]
  (let [tap-log (atom [])
        result (with-redefs [tap> (fn [v] (swap! tap-log conj v) true)]
    {:log @tap-log :result result}))
Is about as good as it gets.

Patrick Brown17:04:20

Is anyone familiar with the error surrounding Fulcro 2. I’m using Fulcro 3 and trying to incorporate Nubank Workspaces, but keep getting this. I’ve heard it’s due to CLJS no longer using the library, but don’t know how to work around it. The required namespace “goog.debug.Logger.Level” is not available, it was required by “fulcro/logging.cljc” {org.clojure/clojurescript {:mvn/version "1.11.4"} thheller/shadow-cljs {:mvn/version "2.18.0"} nubank/workspaces {:mvn/version "1.1.2"}} Any ideas appreciated.


try adding the fulcrologic/fulcro {:mvn/version "2.8.13"} manually


the fulcro version workspaces uses it outdated and only works with older CLJS versions


dunno if this one is any better but worth trying

Patrick Brown17:04:13

I think this is about to be an ignorant question. Fulcro 3 uses a completely different java style namespace convention, so there shouldn’t be any collision hyjinx right?

Patrick Brown17:04:12

I got no dice on adding Fulcro 2. But I appreciate the idea. I think my clojurescript dep might need to be updated manually. Does that make sense?


no, that is the source of the problem. fulcro2 is old and outdated. it used to rely on an older clojurescript version


it seems to no longer work with the newer versions (ie. 1.11.4)


workspaces seems to be outdated as well, so your only option is downgrading shadow-cljs and cljs


the presence of fulcro3 is unrelated and doesn't matter

Patrick Brown17:04:46

Well, it was nice of you to help. But I think my adventures with Workspaces will come to an end before it started. Cheers!


but looking at workspaces it appears to be using fulcro3?

Patrick Brown17:04:18

In the docs, it explicitly calls for Fulcro 2 namespaces to run the Fulcro specific features.


or both I guess. dunno really

Patrick Brown17:04:00

I know some people are having success with storybook on shadow. That may be a more beaten path for that workflow. As always, you’re a great help.

Eric Dvorsak13:09:05

you can use com.github.awkay/workspaces {:mvn/version "1.0.3"} follow the template here I had the same issue with workspaces and this solved it


Hi, I have a question about Bidi router - I'm trying to use regex to match routes, however behavior is really weird, depending on whether i use capturing groups or not, it matches everything until the last character, while in others it matches only the last character. Anyone have experience with this?


found a solution, according to this post ( it adds some extra regex to make it "safe" so the solution was to add an extra pair of brackets around regex with an empty string and now it works - all explained in the link above.

Quentin Le Guennec20:04:22

not strictly clojurescript related, but is there a way to get Promises return values with the xhrio client?


Not sure what you mean. But if you have a promise and you need to do something with a resolved value, you have to use .then or some wrapper that will use .then itself. There's no way around it except for writing JS code that uses await.

Quentin Le Guennec20:04:17

not exactly what I meant, my bad. I mean that XhrIo.send() use callbacks and I'd like to use promises instead.


Ah, wrap it in a promise then. :) Create a promise manually, send the request in its executor function, and resolve the promise in the send callback.


Only in your case it will be not setTimeout but XhrIo.send.

Quentin Le Guennec20:04:06

Oh yeah, makes sense. Thank you!

👍 1

I'm not sure if this already supported, but in my case has to do with whether i'm using vanilla cljs, or shadow, or if has to do with my IDE/LSP or lein/deps, etc, but I was wondering: Is it possible to Go To Definition for node_module dependencies' source code in the same way that I've heard that one can for Java/jar ones in clj?


In principle it's possible, but I'm not aware of any IDEs that provide the capability.

wow 1

Which IDE do you use?




off topic, @U037TPXKBGS can you get Java/jar ones in clj? in calva?


I've never myself done it, but I've seen references to others doing it in the internet and at work, though perhaps not in calva maybe


I'll edit my op to reflect that


I feel the java part does not work.


Perhaps, I'm new enough to clojure to suggest one not defer to me here, but if you ask in #calva or #clojure I'll look forward to any answers