Fork me on GitHub
#clojurescript
<
2020-03-02
>
mazin04:03:52

ran into an externs issue of vars getting munged when upgrading cljs and migrating over to shadow-cljs that I was hoping someone could help shed light on: the js file is pulled in as an external script src. i had an externs file that looked like:

var Foo = {
 data: {
  Bar: {
   width: 1,
   height: 1
  }
 }
}
and
(fn [evt]
   (when (and (.-data evt) (.-Bar (.-data evt)))
          ...             ))
this used to work with cljs 1.7/figwheel, but migrating to cljs 1.10/shadow-cljs breaks it. two ways to fix have been: 1. prefixing with ^js (`(fn [^js evt]`) 2. changing the top level variable from Foo to evt anyone have insight into why this breaks? is it because var Foo is equivalent to js/Foo in a global context and js/Foo is what is being expected here?

thheller09:03:06

prefixing with ^js is the correct way to do this. if you do you don't need any additional manual externs.

p-himik10:03:28

@U05224H0W Should the above example work with just ^js evt? Or should it also specify ^js (.-data evt) to make sure that .-Bar is not mangled?

thheller10:03:56

^js evt should be enough

👍 4
p-himik10:03:16

Huh. But does that mean that I cannot really put anything that must get mangled inside the objects that must not get mangled? E.g. when passing your own objects into some thirdparty JS library.

p-himik10:03:38

Or rather, I can put but cannot access.

thheller10:03:04

there is not such thing as "must get mangled"

thheller10:03:12

it either is or it isn't ... globally.

p-himik10:03:27

Ah, right. I guess I could come up with some example where you put a CLJS object into a JS object and then attempt to access an internal JS field from the CLJS object. But that's probably contrived beyond being even remotely useful.

thheller10:03:27

as I said ... if a property is not renamed it is never renamed

thheller10:03:33

so the CLJS property will not be renamed either

thheller10:03:51

the only exception is when the closure-compiler knows the type of something

thheller10:03:58

since CLJS is untyped it never knows types

thheller10:03:45

the downside is that is sometimes does NOT shorten names when it could

thheller10:03:11

BUT that is an ok tradeoff for less trouble with externs

p-himik10:03:29

No, I mean something like #js {:js_field some-cljs-object}. :js_field will not be renamed. If you pass it somewhere else, especially from a thirdparty JS library, you should use ^js. But the transpiled code of some-cljs-object will contain renamed fields. Suppose it must have a property called stuff. Without optimizations I can just get it via (.. js-obj -js_field -stuff). With optimizations, I have to mark js-obj with ^js but I may no longer access the -stuff part since it was renamed, as all CLJS code usually goes through advanced optimizations.

thheller10:03:07

I don't follow what you are saying ...

thheller10:03:21

as soon as you tag ^js js-obj -stuff will NOT be renamed

thheller10:03:50

no matter where -stuff is from ... as long as it was part of the :advanced compilation it will NOT be renamed since the ^js caused it to generate externs for stuff

p-himik10:03:10

> no matter where -stuff is from Oh... So if I write something like (defn f [^js x] (.-meta )) then cljs.core.PersistentArrayMap.meta will not be renamed as well, even though x has nothing to do with cljs.core.PersistentArrayMap?

p-himik10:03:37

Damn. That's quite anticlimactic.

thheller10:03:40

UNLESS the closure compiler can tell the type of something

thheller10:03:56

so it might sometimes to able to rename it ... but it is rather unlikely

p-himik10:03:17

I see. Thanks.

thheller10:03:33

the impact is far less than expected in my tests

thheller10:03:47

even common attributes names are usually short and gzip well

thheller10:03:02

we really want the property renaming for the long protocol properties CLJS generates

thheller10:03:11

and you'll never have externs for those so we are fine

p-himik10:03:56

That's good. It's just that I was thinking that it did some type inference clever enough to not mix fields with the same names from different areas together.

thheller10:03:15

the closure-compiler does that yes

thheller10:03:28

BUT as I said .. CLJS is untyped so its really quite pointless

thheller10:03:55

unless you want to start type casting all your CLJS values it really doesn't matter

thheller10:03:20

as soon as it finds one untyped reference to -stuff it won't rename -stuff at all anywhere

p-himik10:03:05

Yeah, I see. > CLJS is untyped Type inference is still possible though. But probably immensely complicated. And given what you said, it's probably not worth it.

thheller10:03:40

(:foo bar) is the most common casse in CLJS code .. that is rarely typed or inferenced

thheller10:03:58

sure basic inference is there for strings and numbers and stuff but thats about it

p-himik10:03:10

Yeah, right.

thheller10:03:16

I used to worry about this but after extensive tests in my works projects it barely ever made a difference of more than 1%

👍 4
mazin15:03:05

thanks. my curiosity was around whether anyone knew what the source of the regression of the behavior was. the given extern used to prevent .-Bar and .-data from being munged, even though Foo wasn't referred to directly. i assume it's due to some change in the closure compiler. moving forward, ill switch to using ^js in these situations. thanks.

thheller16:03:35

turn on externs inference so you catch those things early https://shadow-cljs.github.io/docs/UsersGuide.html#infer-externs

👍 4
lilactown18:03:32

this is a really great writeup that a Chrome engineer did about moving work from the main thread to a web worker: https://docs.google.com/document/d/1nu0EcVNC3jtmUVWL8Gs5eCj2p_984kamNhG2nS9gOC0/edit#heading=h.e6n21l1n04rc

thheller18:03:03

FWIW the new shadow-cljs UI uses approach #2 and it works ok but is hardly worth the more complicated setup overall. same conclusion they came too I guess.

lilactown18:03:57

interesting. do you think that having better APIs/abstractions on top of web workers would tip the equation to being more worth it?

lilactown18:03:38

I imagine something like apollo or datascript being a decent abstraction over something that talks to a web worker. but I don’t know what the packaging / deployment complications are, which might be difficult to abstract over

thheller18:03:39

I have a decent abstraction via EQL queries but thats not really the issue

thheller18:03:50

startup sucks because you have to load everything twice

thheller18:03:59

once for the UI once for the worker

lilactown18:03:32

so in the case of clojurescript, that especially bites since you need to load all of clojure.core etc.

lilactown18:03:45

even if you split your code effectively

thheller18:03:59

yeah with code splitting its fine but workers still take some time to start

thheller18:03:58

my framework hides the async stuff fairly well but it can still be annoying when all your data access is async

lilactown19:03:56

yeah. I imagine that UI state doesn’t make sense to offload, but React Suspense or Reagent-like subscriptions could make it much easier to move data caching off thread

thheller19:03:41

yes the UI does that (without react though)

💪 4
thheller19:03:03

but the UI is still too simple since it just displays stuff ... there would be more benefit if it actually computed stuff in the worker

👍 4