Fork me on GitHub
#clojurescript
<
2021-08-31
>
thheller06:08:13

@thomas611 I don't know what the latest state of electron is or what bundlers are supposed to do with the electron package. it appears to fail because shadow-cljs maybe bundled it incorrectly. or something in your code uses it incorrectly. do you use existsSync somwhere in your code? can't see your code

Pepijn de Vos08:08:54

Uh, how are async fixtures supposed to work?

(use-fixtures :once
  {:before (fn [] (async done (.destroy (pouchdb "test") #(println "before" (done)))))
   :after  (fn [] (async done (.destroy (pouchdb "test") done)))})

(deftest test-put
  (async done
         (println "test") [...]
Result
Testing hipflask-test
test
before #object[cljs.core.async.impl.channels.ManyToManyChannel]
So seems like the :before fixture is completing after the test But the docs seem to suggest this is supposed to work correctly: https://clojurescript.org/tools/testing#async-fixtures

borkdude09:08:15

Does someone have an example how the :target :bundle CLJS works for single node scripts? (context: https://clojurescript.org/news/2020-04-24-bundle-target)

Pepijn de Vos12:08:15

Uhm... dumb question but what's the every? equivalent of alts! , I want to wait for all the results. Seems like it should be super obvious but I fail to find the correct search terms apparently.

manutter5112:08:12

I think you just loop thru all the channels and add each result to a collection of results. Each channel will block until it has a result, but since you’re waiting until you have all the results anyway, it doesn’t matter.

manutter5112:08:31

Or I should say, “park” not “block”.

dnolen12:08:59

@borkdude :bundle isn't for Node

borkdude12:08:18

@dnolen I'm trying to reproduce the following use case using "normal" CLJS: term-size (the newest version) is an ES Module

borkdude12:08:29

So in node.js this doesn't work:

require('term-size');

borkdude12:08:59

so you have to write (I think) a .mjs file which says import 'term-size'

borkdude12:08:12

and I wondering how you would write a CLJS file which compiles to such a thing

borkdude12:08:31

with vanilla CLJS

dnolen12:08:45

@borkdude it cannot be done

borkdude12:08:29

lol, so it's not possible to write a script using normal CLJS where you use the newest term-size ?

dnolen12:08:46

you're asking multiple different things 🙂

dnolen12:08:11

:bundle target gets you out of the problem completely - if you're targeting the web

borkdude12:08:29

ok, we can ignore that one, I'm not targeting the web here

dnolen12:08:30

because someone else is going to solve that problem for you

dnolen12:08:20

prior to :bundle everything was still always possible

dnolen12:08:37

you just had to write some trivial JS to bridge this stuff by hand

dnolen12:08:33

@borkdude something else to consider is that :bundle isn't magic

dnolen12:08:01

all we do is look at node_modules and we generate the required JS for you

borkdude12:08:38

that's why I looked into it: I didn't expect it to be only for browsers

dnolen12:08:13

only really tested for browsers because that's by and large the primary use case

dnolen12:08:34

but your problem is different anyway

dnolen12:08:45

JS bundlers solve this annoying problem for you

dnolen12:08:04

you can always use require

borkdude13:08:23

@dnolen I'm already using dynamic import in nbb, and the require is looking like ["term-size$default :as term-size]. I just want to know how this is normally done in CLJS to be compatible.

borkdude13:08:47

(aside from the unnecessary string, I know)

borkdude13:08:33

so any example that reproduces how to load (the newest) term-size in a CLJS node script is something I would look at

borkdude13:08:42

even with shadow

dnolen13:08:16

@borkdude so you're asking for more clarification - sorry I though this was a new question

borkdude13:08:45

well, the question is just: how does one do this normally in everyday CLJS + nodeJS

borkdude13:08:53

an example would be helpful

dnolen13:08:57

"everyday" changes "everyday" clearly

dnolen13:08:08

in the old days your question didn't even exist

dnolen13:08:54

but there's a tipping point now with .mjs but as far as I can tell the end result is the same as it is for :bundle

dnolen13:08:33

dynamic imports can't be known so you must go through $default

borkdude13:08:50

since nbb is still young and I want to be as compatible as possible with CLJS, I can still make changes: this is why I want to know if there's any precedence here

dnolen13:08:25

we refuse to do anything other than explain how it works in JS

dnolen13:08:54

w/ the caveat that we don't support ES6 import in CLJS and we aren't going to because of all the problems

dnolen13:08:29

it's worth pointing that it's not novel that we're going against the flow here

dnolen13:08:49

we don't even really use Node.js require system at all for ClojureScript itself

dnolen13:08:55

we break all the Node.js "rules"

dnolen13:08:58

and load everything globally

dnolen13:08:54

it's my opinion that trying any attempt fit into the Node.js / JS style or reconcile these problems is ill-considered

borkdude13:08:17

oh I'm not fighting that opinion. but it seems I've got nothing to look at in terms of existing scripts that use ES Modules then. The JS churn just leaks through and there's nothing to do about it in terms of trying to be compatible with anything I guess?

dnolen13:08:12

the thing I don't believe in is papering over it

borkdude13:08:27

yeah, I'm not trying to do that either

dnolen13:08:40

it's hard for my eyes not to glaze over

borkdude13:08:50

but suppose :bundle worked for node scripts, how would the :require for term-size look like

borkdude13:08:58

similar to ["term-size$default :as term-size] ?

dnolen13:08:28

yes, I said that before and above - but probably not obvious 🙂

dnolen13:08:16

the whole point of $ that people missed IMO

dnolen13:08:24

is doesn't have anything to do with ESM at all

borkdude13:08:25

At least I've done my best to not invent something new, that's what I wanted to make sure

dnolen13:08:36

it's strictly more useful and it covers this super annoying case

dnolen13:08:42

so stop trying to make something new

dnolen13:08:53

$ lets you use any object as a namespace

dnolen13:08:09

and after that all of our ns require sugar applies

dnolen13:08:15

instead of this JS BS 🙂

borkdude13:08:28

I've always wondered what all this module stuff was about: just use maps right

borkdude13:08:37

it's not more difficult than that probably

dnolen13:08:31

on our work project $ is used for importing $default but also RN Native Modules

dnolen13:08:58

so clearly we have a thing w/o all these pointless differentiations for what might appear like different situations

dnolen13:08:40

we're not quite there yet

dnolen13:08:50

but the last thing was globals

dnolen13:08:19

the reason that (require [window$console ...]) doesn't work yet is because the symbol has to be resolveable as a namespace

dnolen13:08:33

so that needs more thought

dnolen13:08:46

but the end goal is one way to do everything of interest

dnolen13:08:28

@borkdude that does seem weird to me - you should probably at least try some web compatible case with :bundle

borkdude13:08:14

import * as is from 'ink-spinner'

console.log(Object.keys(is.default));
// prints default

borkdude13:08:38

(this is executed from foo.mjs with npm install ink-spinner)

🙌 2
borkdude13:08:10

is.default.default is the actual thing you want to use from that lib`, the Spinner thing

borkdude13:08:51

if you do this:

import Spinner from 'ink-spinner'

console.log(Spinner);
then it prints:
{ default: [Function: Spinner] }

borkdude13:08:46

note that this happens specifically for this lib (and some others), for most libs there is no such nesting

borkdude13:08:29

perhaps this has to do with TS or so?

dnolen13:08:54

hrm dunno, but if it's like that, probably not our problem

borkdude13:08:38

exactly, I've just implemented $ similar to how CLJS does it: just look up the thing from the module (and any other properties like $default and $foo.bar)

Simon13:08:34

I get an infinite re-render loop when i update the state in a child component. How do i fix this? Here is my app structure:

(defonce app-state (r/atom {:geolocation nil
                            :input {"age" 25
                                    "net-worth" 200000
                                    "monthly-income" 6500
                                    "monthly-expenses" 2500
                                    "investment-return" 8
                                    "investment-tax" 0
                                    "withdrawal-rate" 4}
                            :modal {:active? false}}))


(defn child [app-state]
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        monthly-expenses (get input "monthly-expenses")
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        age (.parseInt js/Number (get input "age"))
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)
        cheapest-city (first (sort (fn [city1 city2] (< (:fire-number city1) (:fire-number city2))) cities-table-data))]
    (swap! app-state assoc-in [:a :path] cheapest-city) ;; This swap causes infinite re-renders
    [:div "nothing"]))


(defn app []
    [child app-state])

borkdude13:08:41

@simon.el.nahas Probably don't use app-state as an argument

borkdude13:08:48

since both arguments and r/atoms trigger re-renders

p-himik13:08:00

Or rather, don't use swap! in the view function.

borkdude13:08:26

yes, but sometimes that's what you want, e.g. in click handlers etc

borkdude13:08:33

that's kind of how reagent is expected to work

p-himik13:08:40

That would be a handler, not the view function itself.

Simon13:08:48

removing app-state the prop still gives the same error

p-himik13:08:07

Just move the chepest-city computation to the level where you already have the required data.

Simon15:08:10

@U2FRKM4TW In this example where would you move the computation to?

(defn child []
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        monthly-expenses (get input "monthly-expenses")
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        age (.parseInt js/Number (get input "age"))
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)
        cheapest-city (first (sort (fn [city1 city2] (< (:fire-number city1) (:fire-number city2))) cities-table-data))]
    (swap! app-state assoc-in [:a :path] cheapest-city)
    [table cities-table-data]))

Simon15:08:49

I have now tried to move the computation. I'm not sure where to, but at least this doesn't work.

(defn get-cities-table-data []
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)]
    cities-table-data))

(defn update-top-city! [cities-table-data]
  (let [cheapest-city (first (sort (fn [city1 city2] (< (:fire-number city1) (:fire-number city2))) cities-table-data))]
    (swap! app-state assoc-in [:a :path] cheapest-city)))

(defn child []
  (let [cities-table-data (get-cities-table-data)]
    (update-top-city! cities-table-data)
    [:div "A component which uses " cities-table-data]))

p-himik15:08:38

> where you already have the required data And you seem to already have all the data at the very top level, right where you have defined app-state. In case you change the input data somewhere, re-compute cheapest-city right there. Never change the state in the render function (as opposed to event handlers).

Simon15:08:25

okay now i get it

p-himik15:08:17

Your code is still conceptually wrong. You should compute cheapest-city right along with defonce app-state. And as borkdude as mentioned - if you find data dependency and state changes hard to manage, take a look at re-frame.

p-himik13:08:12

Don't shift it to some component.

borkdude13:08:16

yeah, because your view triggers the re-render every time it's rendering

Simon13:08:29

I'm not sure i understand. This doesn't fix it:

(defn cheapest-city [cities-table-data]
  (first (sort (fn [city1 city2] (< (:fire-number city1) (:fire-number city2))) cities-table-data)))

(defn child []
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        monthly-expenses (get input "monthly-expenses")
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        age (.parseInt js/Number (get input "age"))
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)
        cheapest-city (cheapest-city cities-table-data)]
    (swap! app-state assoc-in [:a :path] cheapest-city)
    [:div "nothing"]))

Simon13:08:22

This component is the one where it generate the needed data using city-table-data

borkdude13:08:32

it seems you want to have some derived data from your state

borkdude13:08:46

but why do you want to put that back into your state?

Simon13:08:06

since i need to access it from another component far from itself

borkdude13:08:38

perhaps calculate it before you store it in the state and then update the state with that atomically

borkdude13:08:00

or use something like re-frame where you can use subscriptions to derive state from the app state

Pepijn de Vos13:08:13

Y'all got any more of those go gotchas? I'm getting Uncaught TypeError: p.then is not a function when using <p! but when I print the promise at the same spot it's definitely a promise. Actually, I commented out all the <p! and it's still doing it! WTF?

Uncaught TypeError: p.then is not a function
    cljs$core$async$interop$p__GT_c interop.cljs:19
    switch__29745__auto__ main.js line 486 > eval line 775 > eval:10
 

Pepijn de Vos13:08:53

Ok found it nvm

Simon13:08:15

1. i'm not sure i understand the first approach you suggest. 2. I'm also considering re-frame, for my next refactor. But right now I just need to get the cheapest city.

borkdude13:08:27

@simon.el.nahas 1) Instead of storing state and then deriving data + storing that in the state you could do that in one go right?

borkdude13:08:55

Just make one update function through which you update the state

borkdude13:08:02

which also calculates the cheapest city

borkdude13:08:37

or just calculate it over and over if it's not that expensive

Simon14:08:02

so I would have another function like this:

(defn state-updater [old-state]
  ;...
  new-state)

Simon14:08:08

and when would I call it?

Simon14:08:46

keep in mind the cities-table-data is needed in a child of the child

(defn child []
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        monthly-expenses (get input "monthly-expenses")
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        age (.parseInt js/Number (get input "age"))
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)
        cheapest-city (cheapest-city cities-table-data)]
    (swap! app-state assoc-in [:top-city] cheapest-city)
    [:div "a table using " cities-table-data]))

borkdude14:08:25

@simon.el.nahas My idea was to calculate the cheapest city in state-updater so you could just use (:top-city @app-state) directly in your component without any calculations

Simon14:08:31

Where would i call the state updater?

borkdude14:08:25

any time you update the state

Simon14:08:05

i see you are thinking something similar to redux/re-frame...

Simon14:08:27

But then the state updater would take an event also right?

borkdude14:08:34

yeah, re-frame is a little bit different in that the derived state is not in the app db

borkdude14:08:45

but re-frame or similar is optimal for this kind of use case

Simon14:08:32

In this case what would the state-updater do? @borkdude

borkdude14:08:09

sorry, meeting now

Simon14:08:31

Okay thank you for your time

Simon15:08:49

I have now tried to move the computation. I'm not sure where to, but at least this doesn't work.

(defn get-cities-table-data []
  (let [input (:input @app-state)
        cashflows (:cashflows @app-state)
        geolocation (:geolocation @app-state)
        withdrawal-rate (/ (get input "withdrawal-rate") 100)
        cities-table-data (map #(city-table-data % geolocation cashflows withdrawal-rate) cities)]
    cities-table-data))

(defn update-top-city! [cities-table-data]
  (let [cheapest-city (first (sort (fn [city1 city2] (< (:fire-number city1) (:fire-number city2))) cities-table-data))]
    (swap! app-state assoc-in [:a :path] cheapest-city)))

(defn child []
  (let [cities-table-data (get-cities-table-data)]
    (update-top-city! cities-table-data)
    [:div "A component which uses " cities-table-data]))

Pepijn de Vos17:08:38

Is there like an into that deletes keys with nil values? If not, what would be a reasonable way to write it? I guess just a copy of into with something other than conj

Joe Littlejohn17:08:30

If you have a sequence of pairs, maybe (remove (comp nil? second)) before your into? You could also use (filter second), but that would fail if the value might be false. So (filter (comp some? second)) would be better.

Pepijn de Vos17:08:16

That doesn't do what I want. I want nil to act like a dissoc on the map.

Pepijn de Vos17:08:31

Yea I just copied into and replaced conj with my own function and it seems to work.

Pepijn de Vos18:08:32

I'v just released my new library, which is an atom (or ratom!) backed by PouchDB. It's an atom that syncs, hopefully great for writing offline-first collaborative apps in Reagent, Rum, or other reactive libraries. Feedback appreciated. Now, on to actually refactoring my app to use it. https://github.com/NyanCAD/hipflask

Bryce Tichit19:08:16

Hello everyone, I have a question, I'm planning to build a new SaaS in September and I'd like it to be in ClojureScript. I just started JavaScript this year and don't really feel comfortable with it I'd really be better with ClojureScript. The fact is I bought a SaaS boilerplate in JavaScript to speed up everything, I have 2 options to use ClojureScript in addition to this boilerplate 1. Translate the full boilerplate (3000 lines) in ClojureScript 2. If there is way, extends the JS boiletplate with ClojureScript (is that even possible?) 3. Third option would be to begin with a ClojureScript SaaS boilerplate but does that even exist? I'm thinking of executing 1 ASAP, to me it'd take the full month to translate the boilerplate in ClojureScript but I think the time investment is worth it. Would you have an idea on how I could be faster? On the short term it'd make more senses to just use JavaScript with this boiletplate but on the long term I think I'd be faster using ClojureScript that's why it's so important to me to begin with ClojureScript. I'm open to any ideas or recommandations you could have, thanks by advance ! Bryce Tichit

Ryan19:08:51

Hey friends, having another interesting problem with a JS library import using shadow-cljs.. a lib called "timeago.js". I've imported it:

(:require ["timeago.js" :refer [format]]) 
as I would for an import like
import { format } from 'timeago.js';

Ryan19:08:45

I then try and use it with a mapped JS DATE like so:

(format (js/Date. (get note :updated-date))
And nothing.. I think maybe format has a collision?

Ryan19:08:18

also as far as I can tell the. '.js' is in the library name

Ryan19:08:28

Oh, turns out I needed to :rename it like so:

["timeago.js" :rename {format ta-format}]
ANd that did the trick

thheller05:09:07

that shouldn't be necessary? do you maybe have another format in the scope shadowing the :refer? (defn do-something [format] (format (js/Date. ...)) or so?

dnolen19:08:10

@tichit.bryce both 1) & 2) are possible - 2) seems faster since you can convert as you go

dnolen19:08:40

caveats would mostly about whether there will be a lot of marshalling between the two

Bryce Tichit19:08:56

Could you send me some documentation that would talk about possibility number 2? Do you mean that I could write my CLJS code, compile it into javascript and from javascript use the compiled code? Thanks a lot for the answer !

kennytilton22:08:27

You can just call the SaaS boilerplate, or pass it CLJS functions, though you will know more "interop" than most of us by the time you are done. 🙂 https://lwhorton.github.io/2018/10/20/clojurescript-interop-with-javascript.html is one resource, another is a bit older but still good: https://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/ Good luck with the project!

Bryce Tichit22:08:12

Thanks a lot for the information really helpful ! I think I will go with the translation of the boilerplate to CLJS, it's a better choice IMO and I will have a CLJS SaaS boilerplate then that I could reuse multiple times. Interop is going to add a linear cost with the size of the program, I don't want that. Better to pay fixed price right now and reduce complexity in the future

kennytilton22:08:17

"Interop is going to add a linear cost with the size of the program" I do not think so. As a Common Lisper going back into the last century, because so few libraries were available, it was commonplace to use C libraries by writing just enough CL code using C FFI to "wrap" the C library in CL, and then Just Write CL(tm). We wrapped just the bits we needed, and we got on with the fun much more quickly, adding bits as we discovered the need. The most fun ever was doing this with OpenGL. Hell, I even did this with qooxdoo, a JS library, so I did not have to write (much) JS, HTML, or CSS. qooxdoo handled the latter two. This site is a Common Lisp application running on a server pushing JS/qooxdoo to the client. http://tiltonsalgebra.com/# So which will be faster? Translating 3K or wrapping it in a CLJS API?

❤️ 4
kennytilton23:08:33

And when it comes to the long view (I loved your analysis approach!) which makes you a more powerful Clojurian? A 3KLOC slog that makes one library accessible, or becoming a champ at interop (with JS anyway)? With your black belt in interop the world of JS is your oyster, not just one SaaS template. Remember, one of the main ideas behind even creating Clojure was the vast universe of Java libraries that would be just a bit of interop away. Have fun either way!

👍 2
Bryce Tichit12:09:27

Thanks a lot for your feedback @U0PUGPSFR very interesting, your point of view is interesting. It's true that I'm unable to take the optimal decision because I'm only a beginner in Clojure/LISP. I need to play around with the interop first to understand everything but you are right, mastering interop is a powerful skill.

Bryce Tichit12:09:34

In the end I was just thinking, the boilerplate itself like any codebase contains a lot of functions that you would compose in the end at the top-level. In React the top Level would be index.js, what if I write the top level composition in CLJS and use interop to compose JavaScripts functions ? What I mean is that the boilerplate is a JavaScript tree, I'm proposing to only replace the head of the tree in CLJS and then connect that head to the rest of the tree (which is JS) using interop. This should work nope? It seems too easy, there may be mistakes in my reasoning. What do you think? Thanks a lot 🙏:skin-tone-2:

Bryce Tichit12:09:15

Anyways you convinced me that I should go the interop road, thanks a lot 😉

kennytilton18:09:07

np! Hope you do not regret it! 🙂 I think as you work on the rest of the app you will naturally see what your wrapper will have to do with the JS code to get the job done. In some cases you will want to tweak the JS code to make the interop easier. In most cases not. You will quickly develop solid interop skills, then hide the interop in CLJS functions so you can forget interop: then the JS template will seem like a CLJS template. You will also discover the wrapper can offer a better interface than the JS template, by automating things the JS template makes you work at. A trivial case when I wrapped OpenGL3 with Lisp was (struggling with memory) that there was some sequence of commands that had to be surrounded by calls to begin and end. Apparently this was a frequent source of bugs. In CL I just wrote a macro, with-begin-end and never had to think about it. That is quite typical of wrapping things in more powerful languages. Happy coding.

Bryce Tichit10:09:48

Amazing @U0PUGPSFR I cannot wait to have my boilerplate ready with CLJS so I can build the whole app in CLJS. I've been coding since university and used a handful of language, everytime something felt wrong and I cannot be productive enough without ever knowing why. The discovery of LISP dialects and Clojure really changed my life, especially Clojure because it is highly functional programming oriented as well. s-expressions feel like the natural way of programming, I'm running out of money right now to build my SaaS but that's no problem I can easily spend 1 month to sharpen my CLJS stack. On the long term I believe I will have a big advantage 🙂

Bryce Tichit10:09:21

This community is really nice as well !

kennytilton20:09:16

"I've been coding since university and used a handful of language, everytime something felt wrong and I cannot be productive enough without ever knowing why. The discovery of LISP ...." You may as well get started on your submission here: https://web.archive.org/web/20120106121645/http://wiki.alu.org/The_Road_To_Lisp_Survey

Bryce Tichit21:09:10

Very interesting but I cannot submit as this page is now offline 🙂

Bryce Tichit19:08:20

I think I will decide between both options by the end of this week

Bryce Tichit19:08:45

I understand the marshalling problem but don't know how big it would be

borkdude20:08:10

@dnolen (and @thheller): I was able to require term-size and some other ES module using shadow's :esm build type and it yields the same style of "requires" as nbb, so I'm even more confident now that I'm doing the right thing. https://gist.github.com/borkdude/7e548f06fbefeb210f3fcf14eef019e0

borkdude20:08:39

FWIW I didn't get this working using vanilla CLJS. Still interested in a similar setup using the vanilla tooling.