Fork me on GitHub
#cljs-dev
<
2023-05-22
>
dnolen15:05:12

@thheller @borkdude I’m assuming you all handle the ES module problem (in Node.js), are you conditionally generating import or always generating import ?

borkdude15:05:45

I am using shadow's :esm target if I'm using shadow-cljs. For #cherry which is a little alternative CLJS compiler I just generate import directly from :require.

dnolen15:05:46

but you always emit import rather than require

borkdude15:05:58

e.g. in Node.js you cannot use require 1. from modules 2. to load other libraries (unless you use some Node.js specific API, module.createRequire(import.meta.url) For this reason, always import

dnolen15:05:44

but if you use import how does this work from a REPL?

borkdude15:05:22

In #nbb what I do is use dynamic import, always. But this is a CLJS interpreter, not targeted at emitting code, but just executing on the fly. So (require '["whatever-lib"]) executes as a dynamic import of whatever lib and then defines whatever is necessary in the environment regarding referred functions, aliases etc. Each top level REPL expression returns a promise (not automatically wrapped like JS promises) and it's "awaited" result becomes the REPL evaluation result. dynamic imports don't play well with optimizations done by JS tooling though #cherry doesn't have much of a REPL yet and this is indeed one of the unsolved annoying ES6 problems.

dnolen15:05:54

right but that is quite ugly for many reasons wrt. REPL

dnolen15:05:03

(do (require …) …) for example

dnolen15:05:51

not criticizing specifically how you did it

dnolen15:05:01

just pointing probably not acceptable for ClojureScript

borkdude15:05:03

$ npx nbb
Welcome to nbb v1.2.173!
user=> (do (require '["fs" :as fs]) (js/console.log fs))
[Module: null prototype] {
  Dir: [class Dir],
  Dirent: [class Dirent],
  F_OK: 0,

borkdude15:05:11

top-level do is unwrapped

dnolen15:05:16

but aren’t there cases you can come up that won’t work because of the promise?

dnolen15:05:02

or are you saying because the REPL deals w/ promises it works?

borkdude15:05:03

for squint (similar project as cherry, but no immutable data structures, etc) I've implemented something that would be closer to the CLJS compiler problem since it emits code.

$ npx squint-cljs repl
user=> (require '["fs" :as fs])
user=> fs/readFileSync
[Function: readFileSync]
What I've done there is sneakily compile the REPL expression to a JS module, emit this to a file (which you shouldn't have to, base64 also works) and then dynamically import that file. So there I just emit a top level import, no dynamic one

borkdude15:05:15

It's also ugly, but ES6 complicates stuff a lot since it's async + immutable

borkdude15:05:58

but in both cases, the REPL knows that the REPL evaluation result is a promise

borkdude15:05:56

but it does not unwrap promises directly:

user=> (js/Promise.resolve 1)
Promise { 1 }
This is something you should be aware of, the default JS behavior automatically chains a promise result within another promise

dnolen15:05:28

ok I’m curious how shadow-cljs solves the import + REPL problem

borkdude15:05:39

last time I asked the target :esm didn't have a REPL, but curious about hearing more from @thheller

thheller15:05:33

require in a do has never worked in a REPL where IO is required ie. browser?

dnolen15:05:16

hrm, actually you are right - require support is a REPL affordance

thheller15:05:18

(assuming something must actually be loaded)

dnolen15:05:25

you write require , and then something else

dnolen15:05:07

so it would have to be dynamic import

dnolen15:05:16

does shadow-cljs do that?

thheller15:05:20

no, the import is only emitted for :target :esm. which currently doesn't support requiring "new" stuff not part of the build yet

thheller15:05:43

otherwise for regular builds import is added as a prepend, so the closure compiler never sees it

borkdude15:05:23

@thheller What do you mean with "regular build", aside from target esm you don't need import, right?

thheller15:05:23

by regular build I mean "not the REPL"

borkdude15:05:08

is there a target esm REPL then?

thheller15:05:14

sure, when loaded in the browser. just not for node or deno

borkdude15:05:03

ah right, that was the caveat

dnolen15:05:13

@thheller hrm so shadow-cljs just doesn’t support requiring libs in Node.js that for some wild reason declare "module" only

thheller15:05:40

not at the REPL. a regular build can do that just fine

dnolen15:05:01

I get the build case of course

thheller15:05:09

the way I have done it in shadow is emitting import * as some$alias$var from "x". which is added as a prepend and loaded normally via node

thheller15:05:55

(require '["x" :as x]) then maps to that some$alias$var, but as of now does NOT do a dynamic import for create that var if it not already exists

thheller15:05:13

it could certainly do that but a REPL and ESM suck so much

borkdude15:05:02

yep :( I'm considering just doing nested global objects for development in cherry and squint. I mapped "CLJS" namespaces to ES6 modules directly, but this doesn't play well with a REPL for sure

borkdude15:05:33

What is the current status of Google Closure and ES6 output? Still nothing?

thheller15:05:41

yeah, thats why during development :esm maps everything in globalThis to emulate "everything is global" as much as possible

thheller15:05:11

has been supported for ages, with a million unsupported cases 😛

borkdude15:05:11

I meant ES6 module output

thheller15:05:01

impossible to make anything work with that though

borkdude15:05:04

but shadow isn't using this right?

thheller15:05:25

no, doesn't allow me to do half the stuff :esm needs to do

borkdude15:05:13

I wish there would be a more dev-friendly ES6 module format with at least mutable bindings, async or not

dnolen15:05:40

ok, I thought the library that I was looking at only supports module but actually that’s not true - it does have a CJS file in the build dir

dnolen15:05:56

I guess even JS people realize that ESM only in Node.js leads to madness?

thheller16:05:26

progress to adopt ESM as universal has been extremely slow yes

dnolen16:05:06

right I remember chatting about this two years ago - like any day now - but based on how bad this stuff is I think it will never happen

borkdude16:05:10

@U050B88UR #C029PTWD3HR defaults to loading ESM but falls back to CJS via module.createRequire if nothing else is available

dnolen16:05:21

I mean the Node.js REPL appears to have the same problem right?

borkdude16:05:26

yeah I remember your rants and you were of course right ;)

dnolen16:05:36

It is impossible to import in a JS REPL?

borkdude16:05:50

yes, but you can do dynamic import

dnolen16:05:01

but that is so broken

borkdude16:05:18

await import("fs")

dnolen16:05:18

total language design fail

2
borkdude16:05:51

well, yeah, the certainly didn't adopt "top level expression is the compilation unit" idea...

dnolen16:05:51

ok anyways, I’ve collected a good amount of information

dnolen16:05:58

I think my feeling is still “do nothing” 🙂

borkdude16:05:07

Some tools like next.js (I can't keep up with what they have now again, vite, or so?) do hot-reload ES6 modules, but how they do it is a complete mystery to me

thheller16:05:00

I share the "do nothing" sentiment. Always feels like things should settle around this stuff but they somehow don't

borkdude16:05:07

bun, which is a Node.js competitor allows more stuff like sync loading ES6 stuff and mixing import and require, I believe.

mikerod19:05:38

☠️ - this is how I feel after reading this thread (fixed). I never realized how repl-unfriendly this es6 has been. Enlightening at least.

2
mkvlr19:05:12

@borkdude afaik vite works by keeping track of the module graph and reloading the relevant part of the tree by pretending it’s a new module via some query param. Modules can opt into hot-module-reloading (HMR) see https://vitejs.dev/guide/api-hmr.html

borkdude19:05:56

right, still it's pretty gross

borkdude19:05:24

I mean, it's great they solved the problem, but it shouldn't have to be that complex

👍 6
2
dnolen16:05:32

new thread - package.json exports - require vs. import field, does shadow-cljs provide knobs for this? @thheller

thheller16:05:15

no support for exports at all as of now. same nightmare of inconsistent support and undefined uses.

thheller16:05:18

directories, wildcards, ordering matters, ambiguities all over the place. just horrible.

thheller16:05:15

there is a :js-options {:entry-keys ["browser" "main" "module"]} config option that lets you re-order so "module" is picked first for example. just not in the context of package.json "exports"

thheller16:05:37

but I'd reuse this for exports for same. same logic really.

dnolen16:05:22

ok, it also seems way too complex to me, I think I will only solve a couple of cases so that there are some clear examples

dnolen16:05:49

community can submit patches to expand the functionality using the refactored code / tests as launching pad

dnolen16:05:08

all I was really going to do was calculate explicit subpath stuff

dnolen16:05:16

so that react-select/creatable works

dnolen16:05:53

and I guess just always resolve to require for now?

thheller16:05:19

if you add the same simplification that shadow-cljs does for strings you don't really need to do anything

thheller16:05:41

ie. any (:require ["any-string" :as x]) is just passed through, without attempting to resolve for node at all

thheller16:05:31

you only needed to know node_modules packages for symbols, so that (:require [any-sym :as x]) can be checked if it exists

dnolen16:05:43

It’s not possible to change anything here anymore really

thheller16:05:10

you are not breaking anything, just relaxing requirements

thheller16:05:13

symbols you'd still need to check. or is your intent to allow (:require [react-select/creatable :as x])?

thheller16:05:15

for shadow-cljs in any build where it doesn't try to bundle npm dependencies (so node, react-native, etc) it just accepts any string and lets webpack/node/whatever figure it out

thheller16:05:31

so it doesn't need to look at exports for those cases

dnolen16:05:00

I think it is breaking because you could have used a string for a regular CLJS require

thheller16:05:05

for symbols it does the node_modules presence check, just to complain if they don't exist

thheller16:05:28

well you could disallow that. shadow-cljs doesn't allow that, and I have never seen it used in the wild.

thheller16:05:57

if you mean (:require ["clojure.string" :as x])

thheller16:05:39

I always thought that was a bug and not intentional

dnolen16:05:34

so altering it is a problem

borkdude16:05:38

I know these aren't the official CLJS rules, but some tools (shadow, nbb, clj-kondo) rely on string = JS lib, symbol = CLJS lib. Keeps things much simpler

👍 4
dnolen16:05:37

we just don’t rely on it and it been that way for years

dnolen16:05:46

specifically clj-kondo for linting would be wrong here

borkdude16:05:40

I know, but there's no other way for clj-kondo (static analysis) to know whether you're using a JS lib or not (and hence you can use the alias as a JS object or not)

dnolen16:05:24

strings are supported because symbols have character restrictions - that’s it, nothing about JS or not

thheller16:05:07

but clojure namespaces can't have those characters, so it makes sense and everything so much clearer

thheller16:05:15

pretty sure clojuredart treats it that way as well

dnolen16:05:58

at this point I’m just pointing out that it’s just very unclear who would be affected so it’s not really worth altering anymore

dnolen17:05:06

this must go back to 2017/18 at least

dnolen17:05:39

@borkdude btw, you could probably call into the ClojureScript functions to figure this out

borkdude17:05:19

yeah, it could do that, but clj-kondo is pretty unassuming, it works on random .clj(s) files without looking at the rest of your project and incrementally learns more if you throw more code at it

borkdude17:05:47

I haven't gotten any complaints about it, I think most people like the shadow convention. But I understand that making breaking changes is a no-go

👍 2
dnolen18:05:11

the other thing to consider is some point we could parse the entrypoints as we do w/ GCL so we validate usages / :refers etc.

dnolen18:05:49

I’m not saying that clj-kondo should do anything - but figuring this stuff does have value on the ClojureScript side

borkdude18:05:00

yeah at some point it might. we're scratching the surface with Java analysis (source + byte code) now to improve Java interop linting/completions

dnolen18:05:18

I believe this fixes the exports problem for react-select which seems like a more typical problem

mikerod19:05:59

That is nice if it is working for react-select. I’ve seen a few libs in recent times that have this same structure. So that certainly covers the cases I’ve been exposed to so far (I read the threads and know that package.json “exports” can get a lot more messy & we aren’t dealing w/that now).

mikerod19:05:31

I like the refactored code org. It makes it clearer where this stuff happens and I just think its easier to read through than the previous.

dnolen19:05:51

definitely … also you know … TESTS 🙂

🥳 2
😂 2
mikerod15:09:55

I just realized there is a release version of CLJS that has this change. I am going to try it out. Been hitting the same react-select style indexing in many node modules I’ve encountered lately.

mikerod18:09:54

I’m still messing with it, but so far it’s looking good. @codemirror modules were another major example of this and we have a bunch of them that now work with CLJS v1.11.121. Quite nice.

dnolen18:05:55

I also changed the the indexing to include .cjs files because this does seem to occur in the wild

dnolen19:05:55

ok feature time

dnolen19:05:44

w/ GCL adopting await and async fns I think we should support this stuff out of the box

dnolen19:05:26

one question I have of is scope, whether js-await and js-async is sufficient

borkdude19:05:39

I get (js-await ...) but where would you use js-async? Some experimental projects and ClojureDart have used:

(defn ^:async foo [])

dnolen19:05:29

that might be sufficient, honestly I haven’t been following along too closely, so if there’s prior art that is good to know

dnolen19:05:49

but what about the anon fn case?

dnolen19:05:57

^:async (fn [])

thheller20:05:19

it is not sufficient

thheller20:05:51

there are very many cases in the compiler that generate IIFEs. that will break await expectations

thheller20:05:32

let being the most prominent I guess, but pretty much all expressions get wrapped in (function() { return ... })();

thheller20:05:37

so async function() { var x = (function() { return await something() + 1 })() } breaks

thheller20:05:40

oh nvm. didn't see other thread

dnolen19:05:03

that is - macros that just emit the keyword

borkdude19:05:16

Just emitting the keyword won't be enough since expressions that result into compiled fn expressions (to make JS behave as an expression oriented language) also need to be marked async. So (I think) it's necessary to use the CLJS analyzer to remember if you are in an async function and then propagate that to the emitted self-calling fn helpers. I've been doing this work in cherry. E.g.:

$ ./node_cli.js --show --no-run -e '(defn ^:async foo [] (let [x (do (js/await 1) 2 3)] 1))'
import * as cherry_core from 'cherry-cljs/cljs.core.js';
var foo = (async function () {
let x1 = (await (async function () {
 (await 1);
2;
return 3;
})());
return 1;
})
;

export { foo }

borkdude19:05:10

I should have introduced a special form js-await for which I have an issue

borkdude19:05:37

So far the CLJS project (and also shadow) resisted adding async/await support, I'd be happy to contribute or help

dnolen19:05:56

hrm right that is the main problem

dnolen19:05:09

I wonder if GCC optimizes async fns?

dnolen19:05:17

trivial cases like that

borkdude19:05:34

you mean strip out the await if it's not necessary?

dnolen19:05:13

oh but I see, this is like list of side effects

borkdude19:05:33

right, anything can happen

borkdude19:05:40

So far shadow covered this use case by implementing js-await as a macro (shadow.utils.js-await..., don't remember the exact namespace) which just compiled into a .then expression. It was the user-space solution which gets you 90% there (no top level await), but because JS isn't Lisp they have to solve it at the language committee level

hifumi12319:05:51

async/await in CLJS will be super tedious unless let is changed to not compile down to IIFEs, I think

borkdude19:05:39

it's not that complex, at least not how I did it in cherry, just remember some state in the context and pass it down

hifumi12319:05:42

e.g. one may end up doing sth like

(defn ^:async f [x]
  (let [y x]
    (yield ...)
    (await ...)))
and with the current implementation of let, we’d have something like this emitted
(function(){   // <-- inserted by compiler
  yield ...;   // <-- error (must be inside function*)
  await ...;   // <-- error (must be inside async function)
}();

borkdude19:05:19

I assume you saw the example here:

hifumi12319:05:25

also how does recur work in an async context? async iteration is relatively new to ES (though before es2020 people in JS/TS world missed Promise.all and would incorrectly use await in for-each loops)

hifumi12319:05:07

yeah that’s right… I remember searching around the net seeing what would be the pros/cons to async/await in CLJS, and so far it seems like a huge refactoring of the compiler would be needed…

hifumi12319:05:19

with that said, I agree with sufficient effort and refactoring we could likely have async/await in CLJS, but it seems like a huge amount of effort for something that is honestly kinda “solved” by core.async, promesa, and JS interop with promises

thheller20:05:41

simple to do now, and conceptually easy to translate to real await later

borkdude20:05:19

> it seems like a huge refactoring of the compiler would be needed I don't think it will be a huge change, but only a PoC will make this evident I guess

thheller20:05:50

oh I tried. it is a substantial change.

borkdude20:05:15

in cherry I only have a few lines of code that are about async/await and I haven't come across an example that didn't work sufficiently. this is why my hunch was that the change to the CLJS compiler wouldn't have to be that big, but I could be wrong

dnolen21:05:58

@thheller hrm, why couldn't this be done w/ a compiler pass?

dnolen21:05:26

if it is a lot of work - then probably not, but if can be done as a pass then probably we can do this

dnolen21:05:55

re: top level await maybe that is off the table?

borkdude21:05:39

if you're not emitting ES6 modules, then top level await doesn't even work

thheller22:05:43

can't remember the exact details of why this was hard. but just search for function in cljs.compiler. there are a lot of them emitted, with no analyzer involved. so a pass won't do anything for those.

dnolen22:05:19

hrm, that is an interesting point right

dnolen22:05:03

we might not be so disciplined when we emit extra fns so maybe that's a problem