squint

p-himik 2025-09-19T14:15:33.325219Z

Man, web dev... So apparently the people who made web workers usable with modules somehow forgot that there are import maps. Or, if import maps appeared later, people responsible for them somehow forgot that web workers exist. In any case, stuff like import * as squint_core from 'squint-cljs/core.js'; doesn't work in web workers at all. There are workarounds like https://github.com/kungfooman/worker-with-import-map/. But maybe it's possible to make Squint issue imports that don't require import maps at all? Those work just fine.

p-himik 2025-09-19T14:28:26.446689Z

Tried (:refer-clojure :only []) with the hope of manually loading squint_core from the right URL - nah, the original import is still there.

borkdude 2025-09-19T14:32:32.744629Z

you can leave out imports with :elide-imports true

borkdude 2025-09-19T14:34:36.617999Z

how are you using/generating squint code?

p-himik 2025-09-19T16:57:18.874019Z

I have squint.edn with

{:paths      ["squint/"]
 :output-dir "squint/"}
And then I run npx squint compile, that's it.

p-himik 2025-09-19T17:00:27.037669Z

Hmm, --elide-imports/`:elide-imports` will work but that would mean that I'd have to provide proper paths inside each .cljs file. Ideally, I imagine that there would be some option in squint.edn that allows inlining an import map, so that all the imports use the real path from the get go.

p-himik 2025-09-19T17:17:59.452139Z

Ah, wait - :elide-imports removes not just the default import, but all imports. Then I don't know how I'd use it given that I still need squint_core and a bunch of other stuff.

borkdude 2025-09-19T17:20:58.237669Z

@p-himik what if you do the rewriting yourself based on the import? or what if you post-process the JS with esbuild before you use it in the web worker?

borkdude 2025-09-19T17:22:36.286219Z

you could bundle the JS with esbuild, I mean

p-himik 2025-09-19T17:24:31.834949Z

Right now I'm using squint compile with nothing else at all. Adding anything else to the chain would defeat the purpose for me personally - at that point I'd switch to the regular CLJS. Of course, I know that Squint is in a different niche altogether, but a trivial setup was the main point why I wanted to try it and Scittle.

borkdude 2025-09-19T17:25:32.912549Z

sure. what if you just wrote the import map yourself fully? you can do this for other libs. for squint_core I believe there may be an option, not sure, I'd have to check

borkdude 2025-09-19T17:25:59.895169Z

I mean, what if you wrote (:require ["" :as foobar])

borkdude 2025-09-19T17:26:31.204139Z

esbuild isn't a big addition imo btw.

npx esbuild index.mjs --bundle --minify > out.mjs

p-himik 2025-09-19T17:26:37.269379Z

That's exactly what I've been doing for thirdparty libs already, just because I don't want to go into my HTML for some CLJS change. But the import map is required for Squint itself, as far as I can tell.

borkdude 2025-09-19T17:27:01.224769Z

ok, so it's just about squint itself. let me check the options

p-himik 2025-09-19T17:28:31.041779Z

Yeah, by this > that would mean that I'd have to provide proper paths inside each .cljs file. I meant only Squint. In my relatively simple project, all third-party stuff is never imported more than once, so it's pretty much irrelevant.

borkdude 2025-09-19T17:30:55.693069Z

even if you import it more than once it doesn't matter, browsers cache these imports

borkdude 2025-09-19T17:31:10.006139Z

so you're not fetching this lib another time from the network - not even loading it a second time

borkdude 2025-09-19T17:31:43.481649Z

unless you add a parameter to make it unique like

p-himik 2025-09-19T17:31:50.991209Z

Right, I'm just saying that any thirdparty lib is not a concern at all, but Squint imports themselves completely prevent me from using web workers unless I use additional tools.

borkdude 2025-09-19T17:32:15.460799Z

understood. checking if there is an existing option and if not, I'm pretty sure we can make this work

borkdude 2025-09-19T17:36:45.544409Z

so there are several other imports besides the core import that are affected by this:

(case alias
      (squint.string clojure.string) "squint-cljs/src/squint/string.js"
      (squint.set clojure.set) "squint-cljs/src/squint/set.js"
      (if (symbol? alias)
        (if-let [resolve-ns (:resolve-ns env)]
          (or (resolve-ns alias)
              alias)
          alias)
        alias))
"import * as squint_html from 'squint-cljs/src/squint/html.js';\n"
cc/*core-package* "squint-cljs/core.js"
So it's all about making squint-cljs configurable. We can do this pretty easily. Let's see, what would be a good name..

borkdude 2025-09-19T17:38:31.985379Z

--squint-package=...?

p-himik 2025-09-19T17:38:43.807909Z

Just to take into account potential needs of others, or maybe even myself down the line - do you think that maybe it might make sense to extend it to import maps in general? BTW found an old closed issue: https://github.com/squint-cljs/squint/issues/224

borkdude 2025-09-19T17:39:49.672389Z

yes, that issue is closed under the assumption that import maps work :)

p-himik 2025-09-19T17:40:33.987699Z

And if you think that targeting a wider use case is useful, then no need to think of a name. :) :import-maps.

borkdude 2025-09-19T17:41:06.049119Z

yeah we can support import maps maybe but maybe it's annoying to have to specify all package like:

"squint-cljs/src/core.js" "https://
"squint-cljs/core.js" "https://
"squint-src/src/html.js" "htps:///
but I guess you're right, this would take care of maximum flexibility

borkdude 2025-09-19T17:41:29.833459Z

so something like:

--import-maps=<import-maps.json?

borkdude 2025-09-19T17:41:40.813059Z

which would read that file, and replace accordingly?

p-himik 2025-09-19T17:42:15.972619Z

> maybe it's annoying to have to specify all package like: Wait, how would that be affected by the hypothetical new feature? Isn't that already what one has to do?

p-himik 2025-09-19T17:42:56.475199Z

Ah, you probably meant that if the new feature is more focused, then it's just squint-cljs with a single root somewhere, not a line per distinct import.

borkdude 2025-09-19T17:43:29.917589Z

yeah. but it would probably still be nice to have this feature for other libs

p-himik 2025-09-19T17:44:33.943719Z

> so something like [...] Yeah! And I'd be fully content if it's also embeddable in squint.edn as proper EDN and not filthy JSON. :D

borkdude 2025-09-19T17:45:42.274849Z

hehe

borkdude 2025-09-19T17:55:58.515359Z

I'll just add import-maps support for the squint core pkg now so you can experiment with it. Then we'll extend further support. Does it sound good?

borkdude 2025-09-19T17:56:04.355589Z

do you use clojure.string etc too?

borkdude 2025-09-19T17:56:40.320979Z

also import-maps supports "squint-cljs/ as a wildcard prefix, I'll not add support for this yet, just literal replacement so you are not blocked

borkdude 2025-09-19T17:59:31.761559Z

this seems to work what I have locally:

$ cat lib/other_ns.mjs
import * as squint_core from '';
squint_core.println("macros2/debug", (1) + (2) + (3) + (4));
squint_core.println("macros2/debug", (1) + (2) + (3));

borkdude 2025-09-19T17:59:53.816959Z

with:

{:paths ["src" "src-other"
         "resources"]
 :output-dir "lib"
 :copy-resources #{"foo\\.json"  "test\\.json" :css}
 :import-maps {"squint-cljs/core.js" ""}}

borkdude 2025-09-19T18:00:28.689339Z

and no test failures, so I can deploy this for you to test

p-himik 2025-09-19T18:02:35.354199Z

Sure, sounds perfect. Yes, I do use clojure.string. Thanks! Will try it out.

borkdude 2025-09-19T18:03:23.348959Z

ok, so support for clojure.string would be nice too then... let me check real quick

p-himik 2025-09-19T18:08:14.596739Z

Wouldn't that be handled anyway with "squint-cljs/src/squint/string.js" in the import maps?

borkdude 2025-09-19T18:08:57.341319Z

I didn't handle that part of the code yet. but I did now:

$ cat  /Users/borkdude/dev/squint/test-project/lib/other_ns.mjs
import * as squint_core from '';
import * as str from 'squint-cljs/src/squint/string.js';
squint_core.println("macros2/debug", (1) + (2) + (3) + (4));
squint_core.println("macros2/debug", (1) + (2) + (3));

borkdude 2025-09-19T18:09:10.252699Z

oh wait no, it didn't work

borkdude 2025-09-19T18:09:28.498459Z

ah yes:

$ cat  /Users/borkdude/dev/squint/test-project/lib/other_ns.mjs
import * as squint_core from '';
import * as str from '';
squint_core.println("macros2/debug", (1) + (2) + (3) + (4));
squint_core.println("macros2/debug", (1) + (2) + (3));

borkdude 2025-09-19T18:10:18.501719Z

ok, release coming up... a few minutes

borkdude 2025-09-19T18:10:35.588949Z

after that we can add better support for import maps with prefixes. this will take more time and less hacks. :)

borkdude 2025-09-19T18:10:41.245029Z

and will require a Github issue.

borkdude 2025-09-19T18:15:11.787229Z

ok, try squint-cljs@0.8.154

borkdude 2025-09-19T18:15:20.529029Z

GH issues + PRs welcome for improvements ;)

borkdude 2025-09-19T18:15:39.713279Z

:import-maps {"squint-cljs/core.js" ""
                   "squint-cljs/src/squint/string.js" ""}

borkdude 2025-09-19T18:18:29.675119Z

hmm, crap, looking at JS docs, should this have been :importmaps instead?

borkdude 2025-09-19T18:18:45.096079Z

or:

{:importmaps {:imports ...}}

p-himik 2025-09-19T18:20:24.426319Z

Mmm, maybe just :imports then? Since that's the exact feature.

p-himik 2025-09-19T18:21:11.246499Z

Or [:import-map :imports] - I myself wouldn't use importmap because it's ugly and I can't think of a reason why it should remain ugly and correspond perfectly to what must be going on in HTML.

borkdude 2025-09-19T18:21:40.208989Z

yes, I think :import-map :imports'. let me change that in the next iteration and I'll ping you about the change. have to do other stuff now.

p-himik 2025-09-19T18:21:53.719989Z

Sure. Thanks!

p-himik 2025-09-19T18:24:18.242829Z

import * as squint_core from '';
import * as str from '';
Ohhh yeahhh. > WorkerGlobalScope.importScripts: Using ImportScripts inside a Module Worker is disallowed. Ohhhh noooo. Can't have shit with web dev. Not related to Squint though. Just some regular bullshit.

borkdude 2025-09-19T18:26:27.690779Z

I guess you can just not use importScripts maybe?

borkdude 2025-09-19T18:26:40.549339Z

or not use a module worker but a classic one

borkdude 2025-09-19T18:27:26.210189Z

or just pre-bundle the stuff with esbuild anyway. it's not that much of an annoyance (to me at least).

borkdude 2025-09-19T18:29:31.124319Z

in a module webworker you do:

import * from ... 
instead of importScripts(...)

p-himik 2025-09-19T18:29:43.092139Z

The first one is one kind of PITA where I cannot use a non-ESM script without jumping through some hoops. The second one is another kind of PITA where I cannot use Squint to implement that worker. The third one is yet another kind of PITA where I have to host that library on my own server, and it's 11 MB (OpenCV.js). Sigh

p-himik 2025-09-19T18:30:22.133239Z

ChatGPT is in the chaotic evil mode right now:

const legacyCode = `
  // legacy-lib.js contents
  function legacyAdd(a, b) { return a + b; }
  self.legacyAdd = legacyAdd;
`;

const workerCode = `
  ${legacyCode}
  self.onmessage = e => {
    postMessage(self.legacyAdd(e.data[0], e.data[1]));
  };
`;

const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob), { type: "module" });

p-himik 2025-09-19T18:31:54.536779Z

> Using ImportScripts inside a Module Worker is disallowed Like... why? Who hurt them? As if it's impossible to circumvent. Just throwing wrenches in all the gears for no apparent reason.

borkdude 2025-09-19T18:32:05.742609Z

you can use a non-ESM script in an ESM script probably using async await + eval? something like (pseudo-code):

const body = fetch(non-esm-scrip).then(x) => x.text())
await eval(body)

p-himik 2025-09-19T18:34:11.990629Z

Hmm, perhaps.

borkdude 2025-09-19T18:34:24.811209Z

or vice versa: you can use ESM code in a non-ESM worker with async functions. You can use REPL mode for this squint and then load the script in an async function with dynamic import.

async function() { import("my-esm.js") }

borkdude 2025-09-19T18:34:54.226509Z

or maybe you don't even need the squint REPL output for this

borkdude 2025-09-19T18:41:43.357429Z

that's right. you can just import regular ESM code from a non-ESM environment using dynamic import

borkdude 2025-09-19T18:41:47.939399Z

so that should be a way out

p-himik 2025-09-19T18:45:34.140849Z

Like a static JS shim that's used for a web worker from Squint code and by itself requires Squint code? Hmm, that's probably the best way to proceed... Thanks again!

borkdude 2025-09-19T18:47:13.374439Z

I don't know exactly what your setup looks like but the general principle is that async + dynamic import lets you do ESM stuff in non-ESM contexts

borkdude 2025-09-19T18:49:09.962569Z

something like this (chatGPT):


<html>
  <body>
    <script>
      const worker = new Worker("worker.js"); // classic worker
      worker.onmessage = e => console.log("Main thread got:", e.data);
    </script>
  </body>
</html>
worker.js
async function init() {
  // dynamically load the ESM file
  const module = await import('./my-esm.js');

  // call something exported
  const msg = module.greet("world");
  postMessage(msg);
}

init();

p-himik 2025-09-19T20:00:56.326979Z

Seems like that works!

👍 1
p-himik 2025-09-19T20:37:01.861779Z

Oh. Oh god. Why haven't I been using (:x obj) and (let [{:keys [x y]} obj] ...) and (assoc! obj :x 1 :y 2) instead of (.-x obj) and other boilerplate. It's so nice.

borkdude 2025-09-19T20:50:23.067709Z

:)

borkdude 2025-09-19T20:51:16.604559Z

one more note about import map imports: they are ordered and this is significant. so I think we'll have to use something like a vector of pairs or so

borkdude 2025-09-19T20:55:28.695189Z

ok now I convinced chatGPT that this order doesn't matter

borkdude 2025-09-19T20:57:28.930679Z

ah it literally says so in the spec: > • The object properties' ordering is irrelevant: if multiple keys can match the module specifier, the most specific key is used (in other words, a specifier "olive/branch/" would match before "olive/"). >

p-himik 2025-09-19T21:04:46.420289Z

Told you - ChatGPT is in the chaotic evil mode. :D BTW, the macros are supposed to work, right? Trying to measure some performance because moving OpenCV.js to a web worker made it perform about an order of magnitude worse. Doing this

(defmacro trace [form]
  `(let [start# (.now js/performance)
         result# ~form]
     (log [~(str form) "took" (- (.now js/performance) start#)])
     result#))
Which gets compiled into this
var trace = function (_AMPERSAND_form, _AMPERSAND_env, form) {
    return clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol("let")), squint_core.list(squint_core.vec(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol("start__196__auto__")), squint_core.list(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol(".now")), squint_core.list(cljs.core.symbol("js/performance")))))), squint_core.list(cljs.core.symbol("result__197__auto__")), squint_core.list(form)))))), squint_core.list(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol("log")), squint_core.list(squint_core.vec(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(squint_core.str(form)), squint_core.list("took"), squint_core.list(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol("-")), squint_core.list(clojure.core.sequence(squint_core.seq(squint_core.concat(squint_core.list(cljs.core.symbol(".now")), squint_core.list(cljs.core.symbol("js/performance")))))), squint_core.list(cljs.core.symbol("start__196__auto__"))))))))))))))), squint_core.list(cljs.core.symbol("result__197__auto__")))));

};
Which fails with this
ReferenceError: clojure is not defined

borkdude 2025-09-19T21:05:34.869939Z

Macros should be imported only with (:require-macros ...) and should reside in a different file. This is a difference and limitation of squint.

borkdude 2025-09-19T21:06:02.544659Z

isn't the time macro available in squint?

p-himik 2025-09-19T21:06:51.031959Z

I can't just print the time - it's in a web worker, I have to .postMessage it. Well, AFAIK.

p-himik 2025-09-19T21:07:01.886759Z

Gotcha about macros - missed that detail completely.

borkdude 2025-09-19T21:07:44.870789Z

squint doesn't have a "whole project analyzer", it's pretty dumb, so it has to know whether something is a macro or not on a file-by-file basis

borkdude 2025-09-19T21:08:04.559129Z

unless it's a core macro

p-himik 2025-09-19T21:19:11.207149Z

(╯°□°)╯︵ ┻━┻ The web worker version is slower because fuck me, that's why. OpenCV.js itself simply works slower there than on the main thread, that's it.

borkdude 2025-09-19T21:20:45.931789Z

because web workers get less priority or so?

p-himik 2025-09-19T21:22:06.708509Z

ChatGPT says that it could be one of the reasons. And then lists a whole bunch of other potential reasons.

borkdude 2025-09-19T21:24:43.543249Z

maybe you have your developer console open and the cache is disabled, and this is why it's slower, because it's requesting stuff? or no, you're actually measuring a synchronous calculation

borkdude 2025-09-19T21:27:35.014619Z

this maybe makes sense: Workers are about prioritizing. The worker is not really about parallelism, that is more of a side benefit, it’s about concurrency and getting things out of the most valuable thread you have, the UI thread. A web worker isn’t about making something take 2 seconds instead of 4 seconds, it’s about doing that thing with the DOM freezing for 0 seconds.

borkdude 2025-09-19T21:27:39.844669Z

(from a blog I found)

p-himik 2025-09-19T21:29:04.695519Z

Yeah, no staff is being fetched. Tried profiling - the profiler somehow lies and reports all times as less than what the difference in performance.now() shows. Yeah, it makes sense that with a web worker I wouldn't see a performance increase - that's not really what I was doing it for anyway. But I definitely was not expecting 3 FPS where previously I was getting smooth 30 FPS (processing a video).

borkdude 2025-09-19T21:30:50.865959Z

maybe try different browsers?

p-himik 2025-09-19T21:39:21.421269Z

Of course, no significant differences. It seems that: • Web worker performance is slow only at the start - starts with 80 ms, drops down to tolerable 20 ms. Somehow that happens without any reloads. Just each time after starting the processing • However, drawing on an offscreen canvas that was created with .transferControlToOffscreen is just abysmally slow • I'm sending the data at the framerate of my screen (due to requestAnimationFrame), 120 Hz • The main thread implementation is actually able to keep up with that! So it wasn't meager 30 FPS, it was 120 FPS! And even just the processing time of 20 ms on the web worker, without taking retrieving the data and painting it into account, would not be able to cut it

p-himik 2025-09-20T11:05:26.073099Z

The problem was a lack of back pressure. But the symptom of "the data is processed as quickly as at the start, but the rendering itself starts lagging behind severely" was very disorienting. Especially given that I did not control rendering at all (`.transferControlToOffscreen`) and all the drawing functions worked immediately. It wasn't relevant for the non-worker impl because it has back pressure built-in. If the computation skips a frame - that's it, the frame is skipped, it's completely forgotten.