Fork me on GitHub
#shadow-cljs
<
2024-01-04
>
indy20:01:33

I'm trying to use cljs only for the worker, which target type should I choose? npm-module doesn't work because of relative paths, node-script requires a main and still doesn't work, node-library also doesn't work.

indy20:01:44

All other files are in TS. So the web-worker option doesn't apply.

thheller20:01:37

:target :browser with :web-worker true for the module

thheller20:01:45

it doesn't need to be split, it can just be one module

indy20:01:03

Thank you, now I get the following errors:

failed to load goog.debug.error.js ReferenceError: goog is not defined
Cannot use default debug loader outside of HTML documents.

indy20:01:28

{:deps true
 :nrepl {:port 7002}
 :builds {:worker {:target :browser
                   :output-dir "out/worker"
                   :modules {:worker {:entries [worker.main]
                                      :web-worker true}}}}}

indy20:01:12

It actually works. I was experimenting and had started the worker with type module, removing the type fixed it.

indy20:01:16

Thank you so much :)

indy22:01:01

I'm trying to use a node package in the worker but I get the following error:

(ns worker.main
  (:require
   ["comlink" :refer (expose)]
   ["pg" :as pg]))
The required JS dependency "core-util-is" is not available, it was required by "node_modules/stream-browserify/node_modules/readable-stream/lib/_stream_readable.js".

Dependency Trace:
	worker/main.cljs
	node_modules/pg/lib/index.js
	node_modules/pg/lib/client.js
	node_modules/pg/lib/crypto/sasl.js
	node_modules/pg/lib/crypto/utils.js
	node_modules/pg/lib/crypto/utils-legacy.js
	node_modules/crypto-browserify/index.js
	node_modules/create-hash/browser.js
	node_modules/cipher-base/index.js
	node_modules/stream-browserify/index.js
	node_modules/stream-browserify/node_modules/readable-stream/readable-browser.js
	node_modules/stream-browserify/node_modules/readable-stream/lib/_stream_readable.js
I'm guessing it's because the target is browser. Web workers support the node environment and I'd like to use the pg (postgres package). I'm doing all this in electron for context.

indy23:01:55

Doing a js/require worked. This ESM, CommonJS drama is such a headache in the JS world.

thheller08:01:45

this has nothing to do with commonjs/esm as far as I can tell. this is purely electron and how it handles the bridging between node and the browser context. FWIW in this case if you use js/require shadow-cljs will ignore it and not try to bundle it. If you use the ns :require it will try to bundle it, but can't in this case due to it being a node package I guess. So you could also set :js-options {:keep-as-require #{"pg"}} in the build config to achieve the same result as js/require basically

šŸ‘ 1
thheller08:01:57

i.e. keep shadow-cljs from trying to bundle it and let the runtime handle it instead

indy08:01:44

Thanks Thomas, there is a lot of jargon that I find hard to keep up with in the JS build world. When you say shadow won't bundle it and allow the runtime to handle it, you mean it has to imported elsewhere (say in JS code) for it to work at runtime?

thheller08:01:16

when shadow-cljs bundles the code it resolves the JS files on disk, transpiles them and puts them into the resulting JS output it generates. with js/require or :keep-as-require it will just leave a require("that-package") in the resulting JS code, leaving the runtime to look it up and shadow-cljs doesn't look for it on disk. the runtime is then responsible for looking for the files, how it does that is dependent on the runtime specifically

thheller08:01:02

i.e. it may just work (as it does in node usually), or may require extra work on the configuration or user side for the runtime to fulfill that request

thheller08:01:41

e.g. in modern electron renderer/browser contexts it doesn't have access for require for security reasons and you are supposed to use a preload

thheller08:01:27

(this comes from someone you has not used electron in any project ever and only glanced over the docs)

thheller08:01:12

I believe there are still settings to just let you use require directly and ignore the security aspects, no clue though

thheller08:01:06

all this is electron specific and has nothing to do with node/browsers/npm, so its rules apply not those

indy08:01:36

Yes electron renderer process doesn't allow things that can be done in the node env, but the renderer can spawn a web worker that can do "node" things like require and use node only packages like pg which is what I'm doing. I can do these things in the main process and use ipc to communicate with the renderer but at some point I'd like to also target the browser (building a db client just for context) so trying to keep a lot of logic in the renderer. Thanks again for the clear explanation of what happens when I js/require and ofcourse for shadow.

thheller08:01:51

ah I didn't know about the worker thing

thheller08:01:22

you maybe can then just set :js-options {:js-provider :require} in the build config, to let the node thing provide all the dependencies

thheller08:01:16

node packages such as pg don't do well when bundled for the browser otherwise, since they can't possibly work

indy08:01:51

can I do this per module? because only the worker module runs in a node env. I only have the worker module for now but if I set that js-provider will it work when I add other modules that are meant to run on the browser env?

thheller08:01:43

no this is a per build option, so it applies to all modules. :keep-as-require is basically equivalent though, so if you don't mind specifying the packages that shouldn't be bundled that is fine too

šŸ‘ 1
Braden Shepherdson22:01:44

I'm surprised to find that a release build of Metabase is generating a bunch of (nearly) empty files. you can try it out like this:

$ git clone 
$ cd metabase
$ yarn build-release
$ cat target/cljs_release/medley.core.js
var window=global;var $CLJS=require("./cljs_env.js");require("./cljs.core.js");
'use strict';

Braden Shepherdson22:01:51

that's happening to both third party code like medley.core and first-party like metabase.lib.cache.js.

Braden Shepherdson22:01:19

I should mention that the CLJS output is used as a library by some other code, so perhaps this is over-eager tree-shaking? certainly metabase.lib.cache/side-channel-cache is called from some ^:exported functions in :entries namespaces, though.

Braden Shepherdson22:01:21

it looks on further inspection like those empty files are empty because all their functions got inlined into other places. I found the guts of side-channel-cache elsewhere. that seems sound, and I guess that's why it works in prod mode.

Braden Shepherdson22:01:05

the symptom that put me onto this is that if I set :source-maps true in release mode then webpack (actually source-map-loader from npm) chokes on the resulting files. that might be a bug in that library rather than shadow-cljs, though. I'm looking into what a valid source map should look like, and whether these are malformed.

šŸ‘ļø 1
thheller08:01:51

yes, this is due to the compiler moving things to places they are actually used and inlined. :npm-module is a hack basically, to make configuration extra easy it just creates a closure module for each namespace. that doesn't prevent the closure compiler from moving all the code out of that module though. I guess there could by some further analysis of the code to remove the empty modules, but they don't hurt usually

thheller08:01:18

how does it choke on the source maps? could be that they are just generated incorrectly when they are empty? which I don't think is a case I considered šŸ˜›

thheller08:01:56

{"version":3,"file":"medley.core.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"file":"medley.core.js","lineCount":1,"mappings":"A;","sources":[],"names":[],"x_google_ignoreList":[]}}]}

thheller08:01:26

it is indeed basically empty, so I can see how that may break if the consumer expects this to not be empty (as in no sources)

Braden Shepherdson14:01:39

lacking sources does seem to be the pain point, poking around in the source-map-loader and source-map-js library code.

Braden Shepherdson14:01:14

section.consumer._(mapping.source); mapping.source is null which makes the .at(...) throw.

Braden Shepherdson19:01:39

I tweaked the code of node_modules/source-map-js/lib/source-map-consumer.js line 1144 by adding if (!mapping.source) continue;. that lets it progress to the end of the build, but there's a bunch of errors revolving around attempting to open various files that really exist in the codebase, but are referenced under shadow/cljs/constants/metabase/lib/blah/blah.cljc and similar. those files don't exist at all; the target/cljs_release/shadow directory doesn't exist at all. the references appear in source maps like this:

"sections": [
  {
    ...
    "map": {
      ...  
        "sources": [
          "malli/core.cljc",
          "malli/transform.cljc",
          "metabase/domain_entities/converters.cljs",
          "cljs/core.cljs",
        "shadow/cljs/constants/metabase/domain_entities/converters.cljs"
        ],
        "sourcesContent": [
          "(ns malli.core\n ...",
          "(ns malli.transform\n ...",
          "(ns metabase.domain-entities.converters\n  ...",
          ";   Copyright (c) Rich Hickey. All rights reserved.\n; ...",
          ""
        ],
...

Braden Shepherdson19:01:08

so perhaps the (falsy) empty string is the problem there? I haven't got a code reference for it but I bet it's doing something like sourcesContent[i] || fs.readFile(sources[i]) when there's no file to read. perhaps this too is really a bug in the source-map-js library, perhaps it's a spec violation or spec ambiguity in the shadow-cljs output.

Braden Shepherdson19:01:49

I'll take a look at this next week. if it's a case of shadow-cljs breaking the spec, I'll try to fix that. if it's source-map-js making too many assumptions, I'll try to fix that. (but if you know where either of these problems lies I defer to you.)

thheller20:01:14

shadow/cljs/constants/... are basically placeholders. the closure compiler needs a place to move things to that are used in the module and possibly shared. so I add an empty file to the compilation per module, as that was the easiest way to make the compiler do what I wanted it to. gotta love JS for treating "" as falsy I guess šŸ˜›

thheller20:01:47

maybe the easiest fix is to just make not those files empty, maybe just // generated or something

Braden Shepherdson20:01:04

that seems like a simple enough fix, yeah.

thheller20:01:39

what is the command to trigger the failing build part?

thheller20:01:15

WEBPACK_BUNDLE=production NODE_OPTIONS=--max-old-space-size=8196 webpack --bail I assume?

Braden Shepherdson20:01:02

yarn build-release will build the CLJS (success) then the JS (errors). you can also run those parts separately with yarn build-release:cljs and yarn build-release:js.

thheller20:01:33

/mnt/c/Users/thheller/code/oss/metabase/node_modules/source-map-js/lib/array-set.js:109
  throw new Error('No element indexed by ' + aIdx);
        ^

Error: No element indexed by null
    at ArraySet.ArraySet_at [as at] (/mnt/c/Users/thheller/code/oss/metabase/node_modules/source-map-js/lib/array-set.js:109:9)
    at IndexedSourceMapConsumer.IndexedSourceMapConsumer_parseMappings [as _parseMappings] (/mnt/c/Users/thheller/code/oss/metabase/node_modules/source-map-js/lib/source-map-consumer.js:1144:48)

thheller20:01:37

that is the error I assume?

Braden Shepherdson20:01:13

yep, that's the original one.

thheller20:01:21

which map is the above btw? which file name?

thheller20:01:07

good error reporting on the JS side. doens't even tell you which file it failed on šŸ˜ž

Braden Shepherdson20:01:52

it varies based on the exactly order. (I put some console.logs in source-map-js to find out.) any of the nearly-empty files in the release build seems to show this problem. see medley.core.js is one, metabase.lib.cache.js is another.

thheller21:01:30

ok I fixed the first problem. just getting the // generated in there turns out to be difficult. I'm giving it to the closure compiler, but it doesn't seem to use it. need to figure out why

thheller21:01:16

there are so many things I regret about :npm-module. it is such a hack šŸ˜›

Braden Shepherdson22:01:22

that one is more arguably a bug at the other end, if the empty string is not supposed to be falsy.

Braden Shepherdson15:01:16

hm, I tried to run with our deps.edn aimed at the patch you made on master but I'm getting an exception

Caused by: java.lang.ClassNotFoundException: com.google.javascript.jscomp.ShadowCompiler
so it seems like I'm holding it wrong.

thheller15:01:23

I can make a release. wanted to look into the thing for a bit some more but got side tracked

thheller15:01:41

if you are trying to use a :local/root version of shadow-cljs you must run lein javac first. there are some java sources that need to be compiled

Braden Shepherdson15:01:04

I was trying :git/url + :git/sha but that may have the same issue. I'll try that locally.

thheller15:01:06

dunno how you'd do that for a git url

thheller15:01:14

no problem making a release if that helps

Braden Shepherdson15:01:17

yep, that did it. now I can repro the second issue with the contents being "" and I'll see about patching the JS library.

Braden Shepherdson15:01:10

gotcha! yep, there was an if (content) { ... } that was wrongly failing for an empty sourceContent of "". I'll send that as a patch to source-map-js and see what happens.

thheller16:01:40

just found that a hacky workaround I did for some closure compiler code is no longer necessary, so maybe time to clean all this up šŸ˜›

Braden Shepherdson16:01:27

sweet, it's working in a local release-mode JAR of Metabase, so presumably it'll work on our internal cloud build etc.

Braden Shepherdson16:01:51

a release would be handy since I can't check in the :local/root thing.

thheller16:01:03

gimme a sec. I removed the hack that indeed is no longer necessary, but the source map source is still empty even though I'm giving it // generated as the source

Braden Shepherdson16:01:28

could you use a non-comment but empty expression instead?

Braden Shepherdson16:01:00

though I guess Closure compiler would remove that too, unless it had some real side effect that couldn't be ignored.

Braden Shepherdson16:01:01

but no worries in the end - I think the if ("") thing is a bug in the source map consumer library that needs patching. we can revisit some hack to make it not "" if they're really resistant to the patch for some reason.

thheller16:01:26

I tried that, but the code that ends up in the source map is still empty. maybe I'm missing something, been a while since I wrote all this and it was never pretty

thheller16:01:00

yeah I'm confused. I can add a literal console.log to the source and it ends up in the final output but not in the source map

thheller17:01:03

try 2.26.3. in theory the source maps shouldn't contain empty sources as I never give and to the closure compiler, but for me they are still empty regardless. maybe its my setup and works for you šŸ˜›

Braden Shepherdson17:01:57

isn't the closure compiler emptying some of these files by inlining?

thheller17:01:10

yes, but source maps use the sources not the optimized output. so that shouldn't matter.

thheller17:01:51

if you are curious what is sent to the closure compiler you can set :compiler-options {:dump-closure-inputs true} in the build config

thheller17:01:35

that'll write all files to disk in .shadow-cljs/builds//release/closure-inputs/, looks all good but doesn't seem to matter

Braden Shepherdson17:01:37

I'm struggling to visualize this properly, but what about this scenario: ā€¢ my.lib.empty is the one that got inlined away. ā€¢ since it's empty, it now has no .map file with your patch. ā€¢ some.other.code depends on it, therefore there's a my.lib.empty entry in the sources and sourceContents arrays of the source map for some.other.code. ā€¢ they end up empty for not super clear reasons.

thheller17:01:22

correct, thats not what I'm talking about

thheller17:01:21

I mean those shadow/cljs/constants/... files. the placeholder files that are referenced in the source maps

thheller17:01:13

at least as far as the input is concerned

thheller17:01:12

during compilation https://github.com/thheller/shadow-cljs/blob/63b7276c59a9cea1aa252a907c67fec278547cf3/src/main/shadow/build/closure/ReplaceCLJSConstants.java moves constants (i.e. keywords) to those placeholder locations, so they become actual constants

thheller17:01:44

but that is not part of the input. source maps should be using the inputs, but for some reason for those files don't

thheller17:01:33

even when I added console.log("yo") it did not change the source maps for some reason, but the log appeared when running the output, so it was not removed

thheller17:01:15

dunno, likely something obvious but I can't figure it out

Braden Shepherdson17:01:03

2.26.3 works nicely with the patched source-map-js. with the patch reverted (ie. vulnerable to empty sourceContents) it fails as before.

Braden Shepherdson17:01:02

shot in the dark: perhaps shadow-cljs is trying to read those "constants" files somewhere but failing and getting nil; then it gets output with (str contents) or similar producing ""?

thheller17:01:41

no, never reading it

thheller17:01:10

source map generation for release builds is handled pretty much by closure alone

thheller17:01:13

I mean the last ditch workaround would be to just parse the JSON, replace empty sourceFileContents and write it again

thheller17:01:29

but I'm trying to get rid of ugly hacks not adding them šŸ˜›

Braden Shepherdson17:01:24

yup. and that can be Metabase's problem, writing a tiny inline webpack plugin for this. I think browsers are fine with these source maps; it's not shadow-cljs's problem that this JS library has a bug, even if the shadow-cljs output is a bit idiosyncratic. any optimizing compiler from another language to JS would be at risk of this kind of thing, and source maps are supposed to support that.

thheller17:01:58

yeah browsers never had an issue with this as far as I can tell. I mean they are empty, so there is nothing to map šŸ˜›

Braden Shepherdson17:01:06

thanks again for your prompt help with debugging and fixing these issues!

thheller17:01:46

I would just remove them altogether but rewriting the entire source map is a bit overkill