Fork me on GitHub
#clojurescript
<
2022-04-28
>
timothypratley00:04:55

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
thheller04:04:48

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)

thheller05:04:30

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

thheller05:04:53

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

azimpel10:04:35

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

timothypratley16:04:07

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.

thheller17:04:53

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

thheller17:04:52

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

timothypratley18:04:03

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.

thheller18:04:55

"why name munge it" is precisely the cause

thheller18:04:22

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

thheller18:04:54

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

thheller18:04:02

so some places end up renamed while others are not

thheller18:04:29

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!

phronmophobic06:04:04

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.

phronmophobic06:04:53

The short answer is reader conditionals, https://clojure.org/guides/reader_conditionals.

1
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?

phronmophobic07:04:37

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.

phronmophobic07:04:17

I think it's basically the same process as deploying a clj library, https://github.com/clojars/clojars-web/wiki/Tutorial

1
magnars07:04:39

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

👍 1
magnars07:04:58

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?

dvingo12:04:36

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

1
Shuky Badeer12:04:36

Roger that! Thank you!

emccue13:04:36

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

1
pinkfrog08:04:53

Hi. Why the following code doe not return true ?

(instance? (type "") "")

p-himik08:04:44

Because that's how it works in JavaScript:

=> "" instanceof String
false

p-himik09:04:05

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

pinkfrog09:04:39

How can I get the tyepof operator in cljs?

pinkfrog09:04:16

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

genRaiy14:04:40

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 ]

dnolen14:04:01

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
genRaiy18:04:28

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

genRaiy18:04:42

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

genRaiy18:04:55

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

timothypratley19:04:01

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

❤️ 1
dnolen19:04:20

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

chef_kiss 1
dnolen19:04:34

$default is only one case it handles

borkdude19:04:05

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)

genRaiy19:04:00

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

dnolen14:04:52

@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

timothypratley16:04:49

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

dnolen16:04:58

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

dnolen16:04:06

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

timothypratley16:04:07

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

timothypratley16:04:52

sure, I'm happy to share

timothypratley16:04:26

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

dnolen16:04:52

hard to read, gist?

dnolen16:04:31

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

timothypratley16:04:49

Let's start with one 🙂

dnolen16:04:22

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

dnolen16:04:29

which are included by default

timothypratley16:04:53

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

dnolen16:04:22

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

dnolen16:04:49

Promise, etc. etc.

dnolen16:04:56

this all seems very strange

timothypratley16:04:03

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)}))}]

dnolen16:04:20

how is that not simply wrong?

timothypratley16:04:34

Without hinting it, the compiler will produce a warning.

timothypratley16:04:51

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

dnolen16:04:06

I see because you are mixing stuff

dnolen16:04:45

That needs hinting and that doesn't need it

dnolen16:04:52

So warning generated

timothypratley16:04:53

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

timothypratley16:04:34

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

timothypratley16:04:31

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

dnolen16:04:58

But why not separate, js requires are poisoned

timothypratley16:04:58

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

timothypratley16:04:04

For example the local storage stuff

timothypratley16:04:18

the interop is contained in a ns

timothypratley16:04:43

but other things look at the things it produces.

timothypratley16:04:34

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.

timothypratley16:04:33

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

dnolen16:04:20

You don't need to warn most of the time

dnolen16:04:28

The calls are poisoned

dnolen16:04:33

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

dnolen16:04:09

Rather than incidental stuff that got tacked on

timothypratley16:04:43

Is this a better bottom level example?

timothypratley17:04:01

It wont work without type hinting.

dnolen17:04:03

so yucky 🙂

dnolen17:04:14

none of the Promise hints should be necessary

timothypratley17:04:43

I agree completely.

dnolen17:04:44

exactly one hint is needed in this file

dnolen17:04:55

^js/firebase.firestore.UserCredential

dnolen17:04:14

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

dnolen17:04:22

all JS imports are poisoned

dnolen17:04:40

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

dnolen17:04:43

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

dnolen17:04:38

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

dnolen17:04:58

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

dnolen17:04:55

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

dnolen17:04:03

adding one type hint (modulo inference bugs)

dnolen17:04:30

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

dnolen17:04:27

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

dnolen17:04:35

if you have to write a hint - something is wrong

dnolen17:04:23

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

timothypratley17:04:24

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

timothypratley17:04:58

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

timothypratley17:04:14

why? I don't know

dnolen17:04:23

this is a Firebase problem right?

timothypratley17:04:35

If I type hint it, it works

dnolen17:04:37

oh you're just talking about your thought process

dnolen17:04:44

this case is simply a bug

dnolen17:04:08

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

timothypratley17:04:45

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

timothypratley17:04:53

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

timothypratley17:04:06

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

timothypratley17:04:13

I didn't recognize it as a bug.

dnolen17:04:21

not calling you out specifically sorry here

dnolen17:04:02

this would appear to catch your case

dnolen17:04:18

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

dnolen17:04:53

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

dnolen17:04:06

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

dnolen17:04:21

all returns from such a call are also poisoned

dnolen17:04:33

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

dnolen17:04:13

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

dnolen17:04:18

function returns are also inferred

dnolen17:04:53

if you understand this then you can identify bugs

dnolen17:04:12

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

dnolen17:04:18

(and what might be missing)

timothypratley17:04:58

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

dnolen17:04:19

should be trivial to write a test for this

dnolen14:04:09

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?

djblue16:04:38

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.

djblue16:04:13

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

djblue16:04:13

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)]
                 (f))]
    {: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.

thheller17:04:18

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

thheller17:04:34

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

thheller17:04:42

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?

thheller17:04:24

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

thheller17:04:44

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

thheller17:04:00

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

thheller17:04:21

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!

thheller17:04:48

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.

thheller17:04:22

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 https://github.com/fulcrologic/fulcro-template/blob/master/shadow-cljs.edn I had the same issue with workspaces and this solved it

ns17:04:21

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?

ns18:04:00

found a solution, according to this post (https://github.com/juxt/bidi/issues/141) 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?

p-himik20:04:08

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.

p-himik20:04:00

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.

p-himik20:04:34

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

Quentin Le Guennec20:04:06

Oh yeah, makes sense. Thank you!

👍 1
rayat23:04:04

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?

timothypratley00:04:52

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

wow 1
timothypratley00:04:59

Which IDE do you use?

rayat02:04:31

Vscode/Calva

pinkfrog05:04:50

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

rayat05:04:14

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

rayat05:04:42

I'll edit my op to reflect that

pinkfrog05:04:49

I feel the java part does not work.

rayat06:04:40

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