cljs-dev

dnolen 2023-05-22T15:04:12.096309Z

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

borkdude 2023-05-22T15:05:45.482689Z

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.

dnolen 2023-05-22T15:08:46.382969Z

but you always emit import rather than require

borkdude 2023-05-22T15:09:58.852159Z

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

dnolen 2023-05-22T15:12:44.108889Z

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

borkdude 2023-05-22T15:15:22.054289Z

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.

dnolen 2023-05-22T15:16:54.283299Z

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

dnolen 2023-05-22T15:17:03.481279Z

(do (require …) …) for example

dnolen 2023-05-22T15:17:51.945219Z

not criticizing specifically how you did it

dnolen 2023-05-22T15:18:01.238129Z

just pointing probably not acceptable for ClojureScript

borkdude 2023-05-22T15:18:03.388879Z

$ 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,

borkdude 2023-05-22T15:18:11.433149Z

top-level do is unwrapped

dnolen 2023-05-22T15:19:16.488309Z

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

dnolen 2023-05-22T15:20:02.438979Z

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

borkdude 2023-05-22T15:22:03.326659Z

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

borkdude 2023-05-22T15:22:15.942019Z

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

borkdude 2023-05-22T15:22:58.704519Z

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

borkdude 2023-05-22T15:23:56.591689Z

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

dnolen 2023-05-22T15:33:28.191369Z

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

borkdude 2023-05-22T15:36:39.741399Z

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

thheller 2023-05-22T15:38:33.778969Z

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

dnolen 2023-05-22T15:39:16.016899Z

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

thheller 2023-05-22T15:39:18.685769Z

(assuming something must actually be loaded)

dnolen 2023-05-22T15:39:25.608089Z

you write require , and then something else

dnolen 2023-05-22T15:42:07.532469Z

so it would have to be dynamic import

dnolen 2023-05-22T15:42:16.033419Z

does shadow-cljs do that?

thheller 2023-05-22T15:44:20.965409Z

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

thheller 2023-05-22T15:44:43.860229Z

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

borkdude 2023-05-22T15:45:23.108909Z

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

thheller 2023-05-22T15:46:23.091119Z

by regular build I mean "not the REPL"

borkdude 2023-05-22T15:47:08.466509Z

is there a target esm REPL then?

thheller 2023-05-22T15:47:14.344359Z

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

borkdude 2023-05-22T15:48:03.082849Z

ah right, that was the caveat

dnolen 2023-05-22T15:49:13.489479Z

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

thheller 2023-05-22T15:49:40.009689Z

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

dnolen 2023-05-22T15:50:01.438959Z

I get the build case of course

thheller 2023-05-22T15:51:09.376619Z

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

thheller 2023-05-22T15:51:55.284429Z

(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

thheller 2023-05-22T15:52:13.726519Z

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

borkdude 2023-05-22T15:53:02.004789Z

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

borkdude 2023-05-22T15:53:33.582319Z

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

thheller 2023-05-22T15:53:41.978949Z

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

thheller 2023-05-22T15:54:11.394259Z

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

borkdude 2023-05-22T15:54:11.675309Z

I meant ES6 module output

thheller 2023-05-22T15:57:01.190419Z

impossible to make anything work with that though

borkdude 2023-05-22T15:57:04.110489Z

but shadow isn't using this right?

thheller 2023-05-22T15:57:25.549929Z

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

borkdude 2023-05-22T15:59:13.681629Z

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

dnolen 2023-05-22T15:59:40.813559Z

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

dnolen 2023-05-22T15:59:56.916219Z

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

thheller 2023-05-22T16:00:26.662379Z

progress to adopt ESM as universal has been extremely slow yes

dnolen 2023-05-22T16:01:06.580839Z

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

borkdude 2023-05-22T16:01:10.743079Z

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

dnolen 2023-05-22T16:01:21.491569Z

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

borkdude 2023-05-22T16:01:26.804479Z

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

dnolen 2023-05-22T16:01:36.176919Z

It is impossible to import in a JS REPL?

borkdude 2023-05-22T16:01:50.700579Z

yes, but you can do dynamic import

dnolen 2023-05-22T16:02:01.990929Z

but that is so broken

borkdude 2023-05-22T16:02:18.004649Z

await import("fs")

dnolen 2023-05-22T16:02:18.252879Z

total language design fail

🤦 1
borkdude 2023-05-22T16:02:51.595799Z

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

dnolen 2023-05-22T16:03:51.249569Z

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

dnolen 2023-05-22T16:03:58.487309Z

I think my feeling is still “do nothing” 🙂

borkdude 2023-05-22T16:04:07.524279Z

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

thheller 2023-05-22T16:09:00.629639Z

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

borkdude 2023-05-22T16:11:07.114999Z

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

2023-05-22T19:02:38.910949Z

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

1
mkvlr 2023-05-22T19:29:12.753129Z

@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

borkdude 2023-05-22T19:30:56.573879Z

right, still it's pretty gross

borkdude 2023-05-22T19:31:24.198249Z

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

👍 3
👍🏻 1
dnolen 2023-05-22T16:05:32.823379Z

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

thheller 2023-05-22T16:10:15.388089Z

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

thheller 2023-05-22T16:10:55.770669Z

https://webpack.js.org/guides/package-exports/

thheller 2023-05-22T16:11:18.536339Z

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

thheller 2023-05-22T16:13:15.655859Z

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"

thheller 2023-05-22T16:14:37.852549Z

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

dnolen 2023-05-22T16:15:22.515429Z

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

dnolen 2023-05-22T16:15:49.809259Z

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

dnolen 2023-05-22T16:16:08.380619Z

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

dnolen 2023-05-22T16:16:16.476809Z

so that react-select/creatable works

dnolen 2023-05-22T16:16:53.185249Z

and I guess just always resolve to require for now?

thheller 2023-05-22T16:26:19.654019Z

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

thheller 2023-05-22T16:26:41.275009Z

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

thheller 2023-05-22T16:27:31.863649Z

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

dnolen 2023-05-22T16:27:43.294659Z

It’s not possible to change anything here anymore really

thheller 2023-05-22T16:28:10.589429Z

you are not breaking anything, just relaxing requirements

thheller 2023-05-22T16:29:13.823209Z

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

thheller 2023-05-22T16:30:15.543179Z

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

thheller 2023-05-22T16:30:31.583189Z

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

dnolen 2023-05-22T16:31:00.941149Z

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

thheller 2023-05-22T16:31:05.095489Z

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

thheller 2023-05-22T16:31:28.659579Z

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

thheller 2023-05-22T16:31:57.202089Z

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

thheller 2023-05-22T16:32:39.734979Z

I always thought that was a bug and not intentional

dnolen 2023-05-22T16:33:02.661399Z

not a bug

dnolen 2023-05-22T16:33:34.603839Z

so altering it is a problem

borkdude 2023-05-22T16:34:38.359489Z

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

👍 2
dnolen 2023-05-22T16:42:37.520259Z

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

dnolen 2023-05-22T16:42:46.276679Z

specifically clj-kondo for linting would be wrong here

borkdude 2023-05-22T16:43:40.757349Z

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)

dnolen 2023-05-22T16:44:24.865639Z

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

thheller 2023-05-22T16:58:07.389219Z

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

thheller 2023-05-22T16:58:15.489799Z

pretty sure clojuredart treats it that way as well

dnolen 2023-05-22T16:59:58.964369Z

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

dnolen 2023-05-22T17:00:06.718989Z

this must go back to 2017/18 at least

dnolen 2023-05-22T17:01:39.909899Z

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

borkdude 2023-05-22T17:50:19.940549Z

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

borkdude 2023-05-22T17:50:47.529809Z

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

👍 1
dnolen 2023-05-22T18:24:11.542819Z

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

dnolen 2023-05-22T18:24:49.417469Z

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

borkdude 2023-05-22T18:26:00.184399Z

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

dnolen 2023-05-22T18:58:18.458139Z

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

2023-05-22T19:08:59.364989Z

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

2023-05-22T19:09:31.340899Z

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.

dnolen 2023-05-22T19:16:51.214789Z

definitely … also you know … TESTS 🙂

😂 1
🥳 1
2023-09-15T15:57:55.153439Z

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.

2023-09-15T18:30:54.693799Z

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.

dnolen 2023-05-22T18:58:55.632119Z

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

dnolen 2023-05-22T19:00:55.624209Z

ok feature time

dnolen 2023-05-22T19:01:44.670179Z

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

dnolen 2023-05-22T19:03:26.003249Z

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

borkdude 2023-05-22T19:04:39.328329Z

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

(defn ^:async foo [])

dnolen 2023-05-22T19:06:29.165049Z

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

dnolen 2023-05-22T19:06:49.640959Z

but what about the anon fn case?

dnolen 2023-05-22T19:06:57.955499Z

^:async (fn [])

borkdude 2023-05-22T19:11:29.387799Z

yes

thheller 2023-05-22T20:04:19.508849Z

it is not sufficient

thheller 2023-05-22T20:04:51.548499Z

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

thheller 2023-05-22T20:06:32.825249Z

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

thheller 2023-05-22T20:07:37.436459Z

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

thheller 2023-05-22T20:09:40.932869Z

oh nvm. didn't see other thread

dnolen 2023-05-22T19:04:03.166459Z

that is - macros that just emit the keyword

borkdude 2023-05-22T19:11:16.083269Z

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 }

borkdude 2023-05-22T19:12:10.911209Z

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

borkdude 2023-05-22T19:13:37.264089Z

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

dnolen 2023-05-22T19:18:56.049159Z

hrm right that is the main problem

dnolen 2023-05-22T19:20:09.239679Z

I wonder if GCC optimizes async fns?

dnolen 2023-05-22T19:20:17.722249Z

trivial cases like that

borkdude 2023-05-22T19:20:34.798649Z

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

dnolen 2023-05-22T19:21:13.918149Z

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

borkdude 2023-05-22T19:21:33.061749Z

right, anything can happen

borkdude 2023-05-22T19:23:40.139129Z

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

hifumi123 2023-05-22T19:50:51.917629Z

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

borkdude 2023-05-22T19:51:39.918709Z

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

hifumi123 2023-05-22T19:51:42.454009Z

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)
}();

borkdude 2023-05-22T19:52:19.139939Z

I assume you saw the example here:

hifumi123 2023-05-22T19:52:25.714529Z

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)

hifumi123 2023-05-22T19:53:07.746429Z

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…

hifumi123 2023-05-22T19:55:19.966919Z

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

thheller 2023-05-22T20:10:17.496319Z

imho js-await should look like this https://clojureverse.org/t/promise-handling-in-cljs-using-js-await/8998

thheller 2023-05-22T20:10:41.661999Z

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

borkdude 2023-05-22T20:11:19.451089Z

> 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

thheller 2023-05-22T20:11:50.179349Z

oh I tried. it is a substantial change.

borkdude 2023-05-22T20:18:15.440029Z

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

dnolen 2023-05-22T21:13:58.794789Z

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

dnolen 2023-05-22T21:15:26.472589Z

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

dnolen 2023-05-22T21:15:55.769979Z

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

borkdude 2023-05-22T21:18:39.684709Z

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

thheller 2023-05-22T22:17:43.320109Z

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.

dnolen 2023-05-22T22:39:19.806109Z

hrm, that is an interesting point right

dnolen 2023-05-22T22:40:03.950509Z

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