Generating ES Modules (Browser, Deno, ...)

Doing what exactly? :browser is still the best option for everything browser based.

I’d be interested the following:

  1. using snowpack.dev for local caching. We already load deps like plotly or vega on demand using d3-require but they currently don’t work when offline.
  2. also advanced compiling modern js (typescript) deps like GitHub - codemirror/view: DOM view component for the CodeMirror code editor assuming that the emitted js is Closure compatible.
  3. enabling easy consumption of our ClojureScript code from js
  4. the isolation provided by ESM allowing us to run different versions of the same code alongside each other

After writing the above it’s beginning to dawn on me that only the last two points are really related to switching to ESM modules for our ClojureScript build, is that correct?

Sort of yes.

  1. You can also do this with :target :browser. The tricky part starts when the dependencies you are importing this way (eg. dynamic import()) also start importing other dependencies. Since shadow doesn’t know about these you may end up with many different versions of certain things. Not a big deal for your project but potentially blocker for others.

  2. Not related to :target :esm at all. :js-provider controls this. In theory you could put all the JS on the classpath but the odds of that working successfully are not that great. All ESM code on the classpath will the go through :advanced. Technically there could be a variant of :js-provider :shadow that tries to put some node_modules code through :advanced as well but I’d say it is way too early for that and the overall messiness of ESM on npm needs to clear up a little first. Most of it isn’t actually “standard” and relies on some new idioms the JS world invented (in particular webpack) that aren’t standard or even documented fully. My hope for ESM “fixing” things in the npm ecosystem has disappeared. Figuring all this stuff out is painful and I still don’t have a clue where this is going. Definitely waiting for things to settle before investing more energy here.

  3. That would likely improve with ESM yes.

  4. Only really possible with release builds as the watch/compile builds fake ESM and still actually live in the global scope so having two of those would break things again.

:target :esm currently makes the most sense when shadow-cljs is not bundling any JS dependencies at all and instead those are only loaded via true ESM. Currently Deno is the only runtime where this is a practical possibility since everything is ESM/TS. However snowpack might be an option too but I haven’t tried in a long time but that is about the same area as using webpack today. It might work though.

Thanks for all those clarifications @thheller!

Think we’ll give snowpack.dev a try, together with skypack.dev (oh javascript!) and report back. From https://www.snowpack.dev/posts/2021-01-13-snowpack-3-0 you see that it enables you to write this

// you do this:
import * as React from 'react';
// but get behavior like this:
import * as React from 'https://cdn.skypack.dev/react@17.0.1';

So as I understand you get local caching and version pinning from snowpack and ESM modules compatible with the browser and a CDN from skypack.

1 Like

Although my thread is no longer ClojureScript… calcit-js does explored persistent data structure combining with ES module syntax, that says, emitting code with import / export. It’s still a very young project, but in case anyone wants to try:

I read this post earlier. It’s mostly about Rails and their approach to JS but it also mentions a lot of the benefits in using ESM as release artifact.

With HTTP2, you no longer pay a large penalty for sending many small files instead of one big file. A single connection can multiplex all the responses you need. No more managing multiple connections, paying for multiple SSL handshakes. This means that bundling all your JavaScript into a single file loses many of its performance benefits (yes, yes, tree-shaking is still one).

When you bundle all your JavaScript modules in a single file, any change to any module will expire the entire bundle. […]

[…] you no longer need bundling for performance, you can get rid of the bundler entirely! Simply serve each module directly, as their own file, directly to the browser.

Some things I’m wondering:

  • The above sounds really nice in a lot of ways, why is it not?
    • Gzipping would be much less impactful is one thing I can think of.
  • Output files from different CLJS builds are not compatible. Does this effectively make the ESM approach unusable?

Superficially the ESM approach seems similar to how ClojureScript dynamically loads files during development.

I guess whether this is interesting completely depends on how effective this new delivery approach is compared to the bundle(s) strategy. The caching of rarely changing dependencies seems like a pretty great benefit though.

In my view ESM changes nothing in regard to why you’d use a bundler in the first place. All the benefits you get still apply. Many people have done experiments and shown that pure unbundled ESM is still way way slower than bundled/optimized JS. You can still gain some of the benefits by using ESM as the final target though.

I don’t believe that you’ll write any serious “web app” without a bundler anytime soon.

Yes, as a compile-to-JS language CLJS has the extra challenge of having to put the “big” cljs.core somewhere and :advanced isn’t extensible later so pre-compiled libs are a challenge. However I don’t think that bundler-less “scales” well enough for even regular JS projects.

Always works well with cherry-picked examples that are directly solved by library X but completely falls apart if you need to do “more”.

1 Like

The title of this post contains ES Modules (Browser, Deno). Note that with the help of @thheller I’ve been able to use this to build a CLJS scripting runtime for Node.js as well:

I was looking for a shadow-cljs Node.js target that supported code splitting, so I could split out dependencies which can be loaded on demand, to get better startup time. The shadow-cljs :esm target supported code splitting, but none of the other Node-related stuff did, so I went with this. Later it turned out that :esm was a good choice in one other aspect: it’s the only target which lets you use dynamic import. Since more and more libraries publish as ES Modules (term-size is an example) I needed the :esm target to build a runtime which allows to load these libraries.
Kudos to @thheller for building this target: I would not have figured this out using CLJS alone (moreover: vanilla CLJS doesn’t work at all with ES modules at the moment). The :esm target came at the right time for something like nbb.

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.