Fork me on GitHub
#shadow-cljs
<
2023-09-20
>
Braden Shepherdson16:09:00

context: Metabase uses shadow-cljs to build a :target :npm-module for our CLJS library code; this library is consumed by JS/TS code compiled with webpack. I'm trying to get the browser REPL working, along with CLJS-style hot reload for CLJS code. (previously the webpack HMR would reload the page because it didn't know how to handle the CLJS updates.) my path so far: • conditionally required shadow.cljs.devtools.client.browser in dev mode • added (js/module.hot.accept ...) to the CLJS code so it tells webpack HMR that it knows how to reload. status: • the REPL connects, and I can send it things from Conjure! • but when I save a file, the shadow reload fails (below) and then the webpack HMR no-op happens. the error:

Failed to load metabase/lib/equality.cljc TypeError: goog.provide is not a function
    at eval (metabase.lib.equality.js:1:6)
    at eval (<anonymous>)
    at goog.globalEval (cljs_env.js:433:1)
    at Object.shadow$cljs$devtools$client$browser$script_eval [as script_eval] (browser.cljs:24:1)
    at Object.shadow$cljs$devtools$client$browser$do_js_load [as do_js_load] (browser.cljs:35:1)
    at browser.cljs:57:1
    at env.cljs:246:1
    at Object.shadow$cljs$devtools$client$env$do_js_reload_STAR_ [as do_js_reload_STAR_] (env.cljs:218:1)
    at Function.cljs$core$IFn$_invoke$arity$4 (env.cljs:254:1)
    at Object.shadow$cljs$devtools$client$browser$do_js_reload [as do_js_reload] (browser.cljs:43:1)
which looks like I'm missing some machinery. I guess the webpack build is replacing the goog.provide/`goog.require` machinery with something else, and the goog library is missing at runtime? is there somewhere I can depend on or require that and it'll Just Work? I couldn't (:require ["goog.base"]) in the same conditional require, at least.

Braden Shepherdson16:09:38

FWIW we do have relatively slow hot reload using the webpack HMR in this configuration. it successfully loads the new code. so perhaps I should be disabling shadow-cljs hot reload and leaning on webpack, with the accept logic to keep it from refreshing the whole page.

Braden Shepherdson16:09:35

(that's working. I'd be glad for it to be faster, though. how do JS devs work like this? 😜 )

thheller17:09:48

in general its a bad idea to have two separate hot reload mechanisms as they will most probably inferere with each other

thheller17:09:19

the error goog.provide is not a function usually means that something is being loading the wrong "context"

thheller17:09:39

there should be a global goog, but depending on what webpack did it might no longer be global

thheller17:09:52

do you set :runtime :browser in the build config?

thheller17:09:09

ah nvm thats the default. so no need to set it

thheller17:09:23

config looks fine

thheller17:09:00

did you consider using :target :esm instead?

thheller17:09:01

where can I see the JS/TS code and webpack config?

Braden Shepherdson17:09:40

well, it's working nicely so far as it goes. I'm trying to figure out how to get the React plugins to detect this and re-render. it's all library code, but if I change how eg. display-name works, any display names on screen don't re-render.

thheller17:09:59

how hard is it to do a local build and getting this to the point where the error occurs?

Braden Shepherdson17:09:43

it requires some changes I haven't pushed yet. with my branch published, it's yarn install; yarn dev and open http://localhost:3000. I don't think there's any active issues in scope for shadow-cljs though. it doesn't seem possible to get webpack's HMR to not reload the files again a few seconds later.

thheller17:09:15

I think webpack has an ignore option for watch, that should block it from trying to hot reload

thheller17:09:44

can't remember what that was called though

Braden Shepherdson17:09:45

it does, but that makes it ignore the files completely, so they never get loaded with the initial build either picard-facepalm

Braden Shepherdson17:09:53

maybe a sufficiently precise regex could exclude everything except the entry point that never changes? I haven't dug into that since the shadow-cljs reloads were failing anyway.

thheller17:09:51

is it possible for webpack/JS/TS to leave an import as it is instead of trying to bundle it?

thheller17:09:18

like if you do import * as CLJS from "./foo.js" it is leasing ./foo.js as it and will let ESM load it? 😛

thheller17:09:58

I have so many nightmares from looking at webpack output, that my first instinct is to always eliminate webpack from the equation

Braden Shepherdson17:09:02

I'm not sure about that. probably not - webpack seems to assume it's got full control in the usual configurations.

thheller17:09:06

at least so that it doesn't touch the CLJS code 😛

thheller17:09:28

that is one big repo

Braden Shepherdson17:09:31

that would be nice, but I don't think we have the option as it stands.

thheller17:09:40

yarn has been going for a while now 😛

Braden Shepherdson17:09:49

I think it's the largest open source Clojure repo

thheller17:09:13

didnt' get to the clj(s) parts yet 😛

Braden Shepherdson17:09:02

the cljs-repl branch has my changes now. if you want to see the error live, you'll need to set :autoload true in the shadow-cljs.edn and then once the app is loaded make an edit to something in src/metabase/lib, like adding a print statement to some function in src/metabase/lib/equality.cljc.

thheller17:09:01

{"via":[{"type":"java.lang.Exception","message":"Failed to load template 'frontend_client/index.html'. Did you remember to build the Metabase frontend?","

Braden Shepherdson17:09:09

you'll see it choke on goog.provide, then a moment later the webpack HMR will succeed.

thheller17:09:14

when opening :3000

thheller17:09:28

something else I need to run?

thheller17:09:00

2023-09-20 19:38:35,425 ERROR routes.index :: Failed to load template 'frontend_client/index.html'. Did you remember to build the Metabase frontend?
[backend] java.lang.IllegalArgumentException: No implementation of method: :render of protocol: #'stencil.ast/ASTNode found for class: nil
[backend]       at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:584)
[backend]       at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:576)
[backend]       at stencil.ast$eval97201$fn__97202$G__97192__97211.invoke(ast.clj:19)

Braden Shepherdson17:09:16

yarn dev should be everything, that's odd.

thheller17:09:30

[backend] 2023-09-20 19:38:28,680 INFO util.fonts :: Reading available fonts from /mnt/c/Users/thheller/code/oss/metabase/resources/frontend_client/app/fonts
[backend] 2023-09-20 19:38:28,694 ERROR routes.index :: Failed to load template 'frontend_client/index.html'. Did you remember to build the Metabase frontend?
[backend] java.lang.IllegalArgumentException: No implementation of method: :render of protocol: #'stencil.ast/ASTNode found for class: nil
[backend]       at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:584)
[backend]       at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:576)
[backend]       at stencil.ast$eval97201$fn__97202$G__97192__97211.invoke(ast.clj:19)
[backend]       at stencil.core$render.invokeStatic(core.clj:80)
[backend]       at stencil.core$render.invoke(core.clj:74)
[backend]       at stencil.core$render_file.invokeStatic(core.clj:87)
[backend]       at stencil.core$render_file.invoke(core.clj:83)

thheller17:09:23

the files exist AFAICT

Braden Shepherdson17:09:04

https://github.com/metabase/metabase#quick-setup-dev-environment I think that should cover everything? yarn dev should run the BE + FE with hot reloading, but maybe we missed something. try bin/build.sh to make sure any one-time assets got created? maybe we broke that first-contact workflow on master.

thheller17:09:06

seems to work on second run

thheller17:09:09

[static-viz] Watchpack Error (stats): Error: EACCES: permission denied, lstat '/mnt/c/pagefile.sys'
[frontend] Watchpack Error (stats): Error: EACCES: permission denied, lstat '/mnt/c/pagefile.sys'
[frontend] Watchpack Error (stats): Error: EACCES: permission denied, lstat '/mnt/c/pagefile.sys'

thheller17:09:14

why is this watching this? 😛

Braden Shepherdson17:09:08

no idea, actually.

thheller17:09:55

so I'm in your cljs-repl branch

thheller17:09:09

shadow-cljs seems to be running

thheller17:09:31

but the client does not connect?

thheller17:09:39

not can I tell if actual CLJS is getting loaded?

thheller17:09:30

websocket.cljs:15 Refused to connect to '' because it violates the following Content Security Policy directive: "connect-src 'self'     *:8080 ws://*:8080 ws://*:9630".

Braden Shepherdson17:09:30

I see the message about the client connecting in the browser console?

thheller17:09:44

already had shadow-cljs running, so 9630 was taken

thheller17:09:40

is there a CLJ repl I can connect to? the one where the CLJ stuff is running? or is that running in shadow-cljs?

thheller17:09:12

I can connect to the shadow-cljs nrepl fine, but is that the backend one?

thheller17:09:21

I don't like these commands where one command runs everything. can I run things separately?

thheller17:09:36

too much terminal noise 😛

thheller17:09:10

ok changed the port, now it seems to connect

thheller18:09:09

I debugged this before ... I just can't remember what the fix was 😛

thheller18:09:54

the double hot reload problem you can solve by outputting to node_modules/metabase-cljs (or pick a dir you like more)

thheller18:09:35

in the JS parts then just from "metabase-cljs/whatever.js" instead of from "cljs/whatever.js". the extra webpack alias entry for this you can then remove

thheller18:09:57

then in the webpack config search for watchOptions and change it to

watchOptions: {
        ignored: /node_modules/
    }

thheller18:09:14

now if I make a change it still fails

thheller18:09:22

but webpack reports [webpack-dev-server] Nothing changed. and does nothing

thheller18:09:57

maybe that also works with the alias setup, tried the node_modules way first but that still webpack HMR'd

thheller18:09:26

I'll try to remember what the fix was for the CLJS reload though

Braden Shepherdson18:09:18

did you try the :target :esm thing? that might change the imports to things that work.

thheller13:09:51

sorry this lead me down a rabbit hole of quirks. I'll see what I can figure out over the weekend probably

Braden Shepherdson13:09:18

😨 I'm concerned that you're diving in really hard on a non-breaking issue. the current state (`:autoload false` and relying on webpack's HMR) is getting the job done - the CLJS code reloads, the REPL works and doesn't need constant reconnecting.

Braden Shepherdson13:09:10

if this has exposed a weak spot you want to dig in and fix fully, that's great. but I don't want you to work hard on this with the goal of unblocking a use case, since we're not really blocked.

thheller13:09:51

don't worry about it. not much related to your case, rather :npm-module in general

Braden Shepherdson13:09:50

sounds good, then.

Braden Shepherdson13:09:52

it's also worth saying out loud that I really appreciate the superb support you give to shadow-cljs users! Metabase has a rare use case - "a CLJS library consumed by JS code" is but "through webpack" is 😱 - and this isn't the first time I've asked a tricky question and got great answers. 👏

Braden Shepherdson15:09:49

hmm, maybe this runs deeper than I thought. I can't seem to eval new things in some contexts? I keep getting errors like

TypeError: Cannot set properties of undefined (setting 'empty_env')
    at ../cljs/cljs.test.js (test.cljs:252:1)
    at options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at fn (hot module replacement:62:1)
    at ../cljs/metabase.lib.js.js (test.cljs:605:1)
    at options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at Object._requireSelf [as require] (hot module replacement:102:1)
    at Object.apply (jsonp chunk loading:444:1)
    at hot module replacement:344:1 
when I try to run a test, or load a namespace which is not (transitively) in the :entries. for example loading a test namespace fails on each deftest. the underlying JS exception is on the code
metabase.lib.equality_test.untyped_map_test = (function metabase$lib$equality_test$untyped_map_test() {
because metabase.lib is defined but metabase.lib.equality_test is not. I thought it would transitively stream the requires into the browser's REPL client.

Braden Shepherdson15:09:53

(maybe there's some missing init code when I do this, so cljs.test.test_vars and other namespaces don't exist in JS land?)

Braden Shepherdson15:09:38

perhaps I'm "holding it wrong" here - I am trying to eval things and use deps like clojure.test that weren't transitively in the :entries! but with :npm-module as it stands it's hard to know how to have the tests in scope in :dev mode without having to list them all 😞 if you supply both :entries and :ns-regexp the latter is ignored.

thheller17:09:14

I'm guessing this is the same issue as hot reload itself

thheller17:09:29

the scope is screwed up and it is mixing things in different scopes

thheller17:09:47

I swear I fixed this before

thheller17:09:55

but can't remember or find it

thheller17:09:28

but this is mostly due to :npm-module being so extremely hacky

Braden Shepherdson18:09:59

I think I follow. I wonder if there's a way to tell webpack in dev mode to treat the (`^:export`ed) functions in the CLJS library as externs or similar, basically to say "these are globals just trust me". then the input and compiled output looks like this:

// foo.ts
import * as Lib from "cljs/some.namespace";

export function someFunction(x: SomeType): AnotherType {
  return Lib.some_function(x);
}


// app.bundle.js or whatever, the compiled output in dev mode
exports.someFunction = (x) => $CLJS.some.namespace.some_function(x);
• the CLJS output isn't part of the build in dev mode • therefore changes to the CLJS don't cause webpack HMR • shadow-cljs hot reload works seamlessly, since it just updates those globals (and the handwritten JS/TS code contains global references to it, so they will never be stale) • throw in (defn ^:dev/after-load tickle-react [] ...) to re-render the UI, and you're done. but I'm sure there's lurking devils all throughout that picture.

thheller18:09:46

problem is that you want webpack to see some of the output, so it can provide the npm dependencies

thheller18:09:23

and then just use the exported vars by name

thheller18:09:53

however that has the problem that this is designed so that the JS parts are inactive, as in used by the CLJS parts but not doing anything on its own

thheller18:09:15

but your use case is the reverse of that as JS seems to be the primary driver

thheller18:09:15

I guess you could setup some sort of coordination mechanism so that the JS only starts once CLJS is "ready"

thheller18:09:03

basically the CLJS just exports global vars

thheller18:09:12

and JS just uses the globals, without any imports

thheller18:09:23

assuming TS can even do that without yelling at you 😛

Braden Shepherdson18:09:41

I'll play around with it.

Braden Shepherdson20:09:16

🎉 I managed to get this working, at least to the point of successfully loading the dev mode app. (120% certain I broke the prod build in the process, but one thing at a time.) here's how it works in dev mode now: • separate shadow-cljs target :dev, :target :browser and :js-provider :external (requiring that generated file from frontend/src/metabase/dev.js, which is dev mode only) • that outputs into target/cljs, ie. outside the frontend/src tree that webpack sees. • webpack has an import alias cljs_dev/foo.bar, so that import "cljs_dev/some.namespace"; really loads the output files (webpack understands this and bundles the files properly) • someone has to require those, so I copied the :entries list into the same dev.js as above, doing import "cljs_dev/each.namespace"; for all of them (that sucks and needs automating but it works for now; see below) • webpack has an externals: [function(...) { ... }] that spots the original "cljs/some.namespace" imports and transforms them into "var" style, which produces the same code as before

var cljs_metabase_lib_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! cljs/metabase.lib.js */ "cljs/metabase.lib.js");
but it "loads" a var module by expecting to find it as a global var. I've confirmed that cljs_metabase_lib_js__WEBPACK_IMPORTED_MODULE_0__ === $CLJS.metabase.lib.js so that feels like most of the pieces are in place! I'm getting a "stale build" warning from shadow-cljs and I'm not sure why; possibly caching in .shadow-cljs/, possibly because of the two ways to import the generated code? I think if I fix that, then the REPL and shadow-cljs hot reloading will be working beautifully. 🤞

Braden Shepherdson20:09:56

ah, I think I solved the staleness/no REPL issue. the yarn build-hot command launches the CLJS and webpack watchers simultaneously. in the old config, both builds start, shadow-cljs finishes, then webpack finishes, then immediately notices the changed CLJS output and builds again. manually editing a regular JS file caused that the reload and now the stale warning is gone and REPL is connected. that's easy enough to fix with separate commands for people who care about CLJS development.

Braden Shepherdson21:09:38

I can't quite find a winning combo here. :target :browser produces a singular JS file and import "cljs_dev/main"; fails with browser bootstrap used in incorrect target because goog.global.document isn't defined. perhaps there's a more right target to use in this different configuration? :npm-module has the same issue with hot reloading that started the conversation, so I tried :browser but it doesn't like being embedded (probably because it's wrapped in a webpack IIFE.

thheller06:09:47

can I see the setup somewhere? I don't quite understand what you are doing now? what is import "cljs_dev/main";? webpack should ONLY see the one :external-index file. it should not be aware of the other CLJS output in any way, i.e. no processing done whatsoever

thheller06:09:02

basically you just add the external index to the webpack build

thheller06:09:14

and let it build all other JS/TS code together with it

thheller06:09:21

then the :browser CLJS build is independent

thheller06:09:27

don't use the $CLJS var. that is entirely a development only thing that doesn't exist in release

thheller06:09:47

any ^:export you have is available is the global scope under that name

thheller06:09:23

so metabase.lib/whatever is just metabase.lib.whatever, or maybe it that makes typescript happier window.metabase.lib.whatever

thheller06:09:50

note that you can also supply the full name from the CLJS, it does not need to be your exact namespace structure

thheller06:09:37

so (defn ^{:export "myCLJS.foo"} hello-world [x] ...) will be available under myCLJS.foo, not whatever.namespace.hello_world

thheller06:09:17

so if I follow correctly your only problem is importing the shadow-cljs :modules into the JS/TS build, instead of just the :external-index file

Braden Shepherdson12:09:10

I juggled several possible setups yesterday; let me take another look now. import "cljs_dev/main"; is exactly requiring the single output file (`main.js`, since there's only one module) from somewhere early in the webpack world, so the file gets included. let me make sure I'm testing the setup you suggested and see how it goes. my memory says it failed on the goog.global.document check, but I'm only ~70% confident I'm remembering the right attempt.

Braden Shepherdson12:09:59

Uncaught Error: browser bootstrap used in incorrect target
    at Object.<anonymous> (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:557959:11)
    at ../../../target/cljs/main.js (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:558100:3)
    at options.factory (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:704:31)
    at __webpack_require__ (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:35:33)
    at fn (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:361:21)
    at ./dev.js (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:510135:71)
    at options.factory (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:704:31)
    at __webpack_require__ (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:35:33)
    at fn (runtime.hot.bundle.js?b6ce8fd471095ef10d4c:361:21)
    at ./app-main.js (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:503708:70)
yeah. it fails early in loading the app on this check https://github.com/thheller/shadow-cljs/blob/d69b2ddbfd36e7b005b4de1d79b073347f247d3f/src/main/shadow/boot/browser.js#L6 I speculate it's because the main.js output file was loaded inside a (function () { ... })() by webpack. I wonder if I can tell webpack to load it in global <script> tag style instead of bundling it; and if that would work.

Braden Shepherdson12:09:43

ah, yeah. goog.global = this || self; but this is set as module.exports in webpack's module loading context, so it's {} not window. I'll see about adjusting how the main.js is loaded to be an unwrapped, global <script> tag.

Braden Shepherdson13:09:05

okay I did that but the timing is busted. before the main.js finishes running, other imports are already failing because window.metabase isn't defined (yet). I'm poking around with the "promise" style of webpack externals to see if I can make it wait properly.

Braden Shepherdson14:09:53

no dice. that broke things in other ways. not surprising; I think you'd have to do async loading "all the way down" for that to work.

Braden Shepherdson14:09:26

but now I think I'm properly stuck. there's two incompatible timing constraints: • I need to load the :js-provider :external webpack bundled file first, so that shadow$bridge is defined before the main.js build loads. • but the main webpacked code is trying to consume main.js globals before it finishes loading in script style, so main.js would need to load first, like in a <head><script> tag.

Braden Shepherdson14:09:11

my gut feeling now is that the way forward is to give :npm-module some love, so that it supports shadow-cljs hot reloading and the REPL, in a way that still plays nice with webpack. (and I'll rework our setup so the output isn't under frontend/src, so webpack's HMR isn't involved.)

thheller18:09:46

Uncaught Error: browser bootstrap used in incorrect target
    at Object.<anonymous> (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:557959:11)
    at ../../../target/cljs/main.js (app-main.hot.bundle.js?1f108a291b8ab1aed5c6:558100:3)

thheller18:09:02

all this tells me is that you are loading the CLJS output after it has been processed by webpack

thheller18:09:07

but it shouldn't even touch it

thheller18:09:38

but yes, that is the loading order issue I mentioned above

thheller18:09:59

you either setup some kind of coordination mechanism, so that the JS waits for the CLJS to load and then starts

thheller18:09:05

or :browser is not an option

thheller18:09:29

I assume there is some kind of "start" function in your JS code

thheller18:09:45

if that start point can wait for everything else to finish it should be fine right?

thheller18:09:55

or do you need to access the CLJS code immediately while the files are loading?

thheller18:09:57

that won't work

Braden Shepherdson18:09:33

I've just finished getting two branches ready with my best effort for both :browser and :npm-module. see https://github.com/bshepherdson/metabase/tree/cljs-repl-browser and https://github.com/bshepherdson/metabase/tree/cljs-repl-npm (I've replaced the README.md with usage notes and a sketch of the approach for each one).

Braden Shepherdson18:09:38

the globals are needed statically, unfortunately. webpack was replacing the imports of CLJS with const Lib = window.metabase.lib.js; or similar, at the top level.

thheller18:09:11

hmm and if you get rid of those? I mean this isn't really needed right?

thheller18:09:43

the actual code can just call metabase.lib.whatever() instead of Lib.whatever()?

Braden Shepherdson18:09:58

theoretically I could replace all the Lib.foo() calls with window.metabase.lib.js.foo() calls and it should work.

thheller18:09:49

or if thats possible with webpack you could take the :external-index file, run it through webpack

thheller18:09:55

then load that file

thheller18:09:57

then the CLJS output

thheller18:09:02

then the webpack regular output?

Braden Shepherdson18:09:11

directly calling the globals would be plausible but unpopular, but it's also not minifier friendly IIUC. or maybe ^:exported functions are always globals even in a release build?

thheller18:09:27

:export is always global yes, well not in :npm-module but in :browser yes

thheller18:09:51

I'm just not sure if its possible to make webpack split out the :external-index content and load it first

thheller18:09:10

I'd assume that is possible but thats way beyond what I know about webpack 😛

Braden Shepherdson18:09:30

I see. that's a plausible path if there are no other options. probably there's even a way to persuade webpack to generate code on seeing import * as Lib from "cljs/some.namespace"; that will do nothing at the top level but access the global when used in the function calls.

Braden Shepherdson18:09:45

I'll play around with that, I don't think it's a big step from the current cljs-repl-browser branch.

thheller18:09:40

I mean :esm is also an option, but since you are using namespaces directly is going to be a little annoying

Braden Shepherdson18:09:57

I don't follow that.

thheller18:09:27

:npm-module you can access each namespace directly, as you are doing in import * as Lib from "cljs/some.namespace";

thheller18:09:35

:esm you setup the modules in the build config

thheller18:09:57

so you'd have import * as Lib from "cljs/that-module"

thheller18:09:10

but rather than just having (defn ^:export foo []) in the CLJS code

thheller18:09:25

you have {:exports {foo some.namespace/foo}} in the build config

thheller18:09:37

so if you have a lot of exports that gets a bit tedious

thheller18:09:25

could also to :exports-var some.namespace/exports and you do the (def exports #js {:foo foo}) in code

thheller18:09:40

ah no wait nevermind, that does't work for ESM

Braden Shepherdson18:09:12

we do have a lot of functions, yeah. but that could be a hook in the build config? (that scans the classpath for .clj{s,c} namespaces and for ^:exported functions inside them.)

thheller18:09:55

no, as a module is the grouping of multiple namespaces

thheller18:09:14

unlike :npm-module where you get a file per namespace

Braden Shepherdson18:09:47

hoist the hook a level higher, where it generates the :modules entirely? 😜

thheller18:09:47

but :modules control what gets built, so without it doesn't know where to start 😛

thheller18:09:07

what bothers me about :npm-module as well as :esm I guess is that the code gets processed twice

thheller18:09:18

so first shadow-cljs+closure compiler does its thing

thheller18:09:21

then webpack does it again

thheller18:09:29

which is wasteful and not optimal

Braden Shepherdson18:09:36

I have hopes for the above scheme to make the access to the globals come later so :browser would work.

thheller18:09:07

yeah I think the best option is 3 way split of the files, that way CLJS and JS stay entirely seperate

thheller18:09:19

and the "shared" JS just gets pushed into the 3rd file

Braden Shepherdson18:09:53

I can make the import return a Proxy so the globals won't be touched until one of the functions is actually called. that plus delaying the app kickoff until the CLJS code is loaded should do it, I think.

thheller18:09:14

do you do anything special that you actually need webpack for? I mean the config looks a bit intimidating but do you actually need it? 😛

thheller18:09:51

ah nvm. I guess you are using images and fonts and other resources

Braden Shepherdson18:09:06

I was wondering that yesterday too. we use it, but I think only for things that shadow-cljs could do... but I don't know how comfy folks would be with that, and I fear how tightly integrated some of the FE folks' tools are with webpack.

thheller18:09:32

let me think about it a bit over the weekend, these all have been issues that have come up many times before

thheller18:09:44

but now having the real project to look at might give me some ideas

thheller18:09:03

maybe I just back of the one reason that makes :npm-module so hacky

thheller18:09:26

could be much less hacky after all if I don't try to isolate the scope

Braden Shepherdson18:09:00

okay, we'll talk next week. I'll let you know if I can get the Proxy hack going. (seems plausible, but who knows.)

thheller10:09:09

ok, should all be fixed with 2.25.6

thheller10:09:24

just use :npm-module, no special settings. should just work out of the box

thheller10:09:02

just need to remove that one js/module reference you have in a CLJS file

thheller10:09:16

CLJS hot-reload and repl works just fine

thheller10:09:44

webpack always says [webpack-dev-server] Nothing changed. so thats maybe a problem or maybe not? not really sure 😛

thheller10:09:04

the CLJS parts however all now just work as intended

Braden Shepherdson16:09:04

😍 fantastic stuff!

Braden Shepherdson16:09:13

hm, I'm seeing shadow-cljs reloading the code successfully, which is great. but I'm also seeing webpack HMR detecting and reloading it, so I'm missing something to stop it. I'm trying to bring back the js/module.hot hacks.

Braden Shepherdson16:09:22

was this working for you in the Metabase repo? I wonder what's different here.

thheller17:09:54

webpack always said nothing changed? and didn't reload anything itself

thheller17:09:47

can't bring back anything working with js/module since that doesn't exist when shadow-cljs reloads the code

thheller17:09:24

I tested on the metabase repo, but I still had :output-dir "node_modules/metabase-cljs" in the build config

thheller17:09:39

and then import ... from "metabase-cljs/whatever.js" in the code

thheller17:09:45

nothing extra in the webpack configs

Braden Shepherdson18:09:44

hmm. maybe there's magic for node_modules. I'll try that.

Braden Shepherdson18:09:26

no, webpack is still recompiling and HMRing for me. can you push your branch?

Braden Shepherdson20:09:03

I found my issue with webpack; I hadn't spelled the watchOptions.ignored properly. it needs to be **/node_modules/metabase_cljs_dev/** or similar; I had node_modules/metabase_cljs_dev/**.

thheller20:09:09

why are you doing this dev thing?

Braden Shepherdson20:09:25

well, the reload and REPL are working nicely. loading namespaces that are outside of the :entries doesn't work, but I'm not overly surprised by that.

thheller20:09:16

why would it not work?

Braden Shepherdson20:09:25

we have cause to let dev and release builds coexist; hence the :dev {:output-dir ".../dev"} split.

thheller20:09:03

what do you gain by doing that? seems just like extra config noise you don't really need?

thheller20:09:21

I mean if you want to try a release build you just make one?

Braden Shepherdson20:09:34

on watchOptions.ignored: I think it wants to match relative import paths like ../../../../node_modules/metabase_cljs/some.namespace.js

Braden Shepherdson20:09:26

on /dev: we often end up doing release builds while having the dev mode watch open at the same time. that was clobbering things and causing issues some months ago.

Braden Shepherdson20:09:20

I agree it makes life more complicated, but I think the alternative is worse, and webpack HMR is sorted out now.

Braden Shepherdson20:09:44

(still no idea why it detects changes for me but not for you; with the watchOptions.ignored set it doesn't emit any HMR messages.)

Braden Shepherdson20:09:03

on loading things that aren't in :entries: is it expected to work if I eg. try to eval a test namespace that isn't part of the build? I'm seeing it doesn't properly require things that aren't already part of the build (eg. test utils) and also that actually running a test fails because in cljs.test.test_vars is undefined. (I'd be happy to include the tests in the build if I were either (a) confident they'd be tree-shaked out of a release build; or (b) there was like :dev {:extra-entries [...]} or :dev {:extra-ns-regexp #"-test"} that get merged together with the regular :entries.

thheller21:09:15

it should work fine to load any namespace over the repl

thheller21:09:54

just remember that cljs.test is extremely macro heavy

thheller21:09:08

but the namespaces should load absolutely fine just like any other

thheller21:09:16

assuming you require or load-file them of course

thheller21:09:23

just in-ns won't do anything

Braden Shepherdson13:09:58

I use Conjure in NeoVim, so :EvalBuf is calling eval-str with the contents of the buffer. IIUC that's not the same thing as a require. I guess I'll take that up with #CK143P6D7

Braden Shepherdson13:09:01

thanks again for all your help on this! this will make our work on sharing business logic between the Clojure backend and JS/TS frontend a much better experience. hot reloads are great, but hooking into the running browser to play with the same query the UI is building is the real killer feature.

👍 1
thheller13:09:10

if you have some repro steps for stuff not working in the REPL let me know. happy to look at it, just need the actual executed REPL commands as I don't use Conjure

Braden Shepherdson13:09:27

I think it's: • :target :npm-module in the browser like Metabase • connect the REPL • choose a namespace not already in :entries, like a test namespace. • (eval-str (slurp the-ns-file)) through the REPL • receive errors

thheller13:09:08

that is not a valid thing to eval at a CLJS REPL

thheller13:09:16

eval-str does not exist, neither does slurp

Braden Shepherdson14:09:01

I guess didn't mean actually run that code in the browser. like as nrepl commands.

thheller14:09:18

also don't exist as nrepl commands 😛

Braden Shepherdson14:09:38

I've got to run but I'll be back later. Conjure (and Vim REPLs generally) are weird.

thheller14:09:54

thats why I mean I need the actual things that are getting eval'd. every editor does its own thing in some way

thheller14:09:56

you can also run clj -A:cljs -M -m shadow.nrepl-debug 3333 .shadow-cljs/nrepl.port

thheller14:09:07

and then connect your editor to nrepl port 3333 instead of the shadow-cljs port

thheller14:09:27

this creates a debug logfile of actual nrepl traffic in target/nrepl-debug

thheller14:09:10

that has all the infos I need

Braden Shepherdson15:09:14

hm, I can't get that to work. it shows [:server-ready 3333 50655] which looks plausible, but then on connecting to 3333 I get "no available JS runtime" even though the browser is connected. if I connect directly to 50655 it works fine.

Braden Shepherdson15:09:04

ah, there we go. https://drive.google.com/file/d/1VS5Aunl6DC0dcZDJ-gePC1cSFJujJdbq/view?usp=sharing that's me eval-ing a (js/console.log "foo") then trying twice to eval a test namespace. that always seems to fail the first time (or maybe I don't wait long enough after landing in the ns?) but the second attempt is what I've typically been seeing.

Braden Shepherdson16:09:08

(if you're not familiar with how vim REPL integration generally works: there's no terminal interface where you type into the REPL. you send the inner or top-level form under the cursor, or the whole file, to the REPL and an output window displays the results. the sticky part here seems to be that "eval the whole file" is done in the same way as any other form, ie. just passing the whole text of the file to be eval'd like anything else.)

thheller20:09:15

your codebase is weird. code I can execute just fine in my shadow-cljs project, blows up in the metabase project

thheller20:09:36

as in it runs very slowely and eventually runs out of memory

thheller20:09:21

do you modify the clojure runtime in some way? its too much code to easily figure out what might be going on

Braden Shepherdson20:09:30

not that I know of. there is a custom test assertion, but that's just a defmethod call I think. mostly it's just a huge codebase, ~113kLOC of non-test Clojure(Script). it's become a performance regression test for clj-kondo and clojure-lsp.

Braden Shepherdson20:09:18

the webpack part of the JS build does crash for OOM every few hours though, if you keep making changes so it does hot reloads.

Braden Shepherdson20:09:01

(for shadow-cljs purposes, the CLJS-facing codebase is much, much smaller than the BE CLJ codebase.)

thheller20:09:37

it appears to be cider-nrepl, at least it showed up in a jstack dump where it shouldn't even be involved

thheller20:09:46

removed it and the thing I was looking at works fine

Braden Shepherdson20:09:27

weird. maybe one of us has a ~/.clojure/deps.edn that's doing something different? I think all the versions are up to date, but perhaps not.

thheller20:09:22

I use Cursive. so no cider-nrepl at all ever

thheller20:09:01

but that wasn't the problem I was looking at, just one that prevented me from looking what I actually wanted to look at 😛

Braden Shepherdson20:09:06

FWIW I just updated my PR with the latest integration of 2.25.6, https://github.com/metabase/metabase/pull/34007/files in case there's something diverging between your tests and mine.

thheller20:09:06

so I'll leave that one for later

thheller20:09:34

I'm fighting with your dev setup right now

thheller20:09:49

its driving me nuts to always start everything in one command, so trying to split it up

thheller20:09:12

but now I started shadow-cljs separately but for some reason webpack isn't using that build output?

thheller20:09:20

or rather the REPL is not connecting

thheller20:09:12

basically I just removed 'yarn build-hot:cljs' from the dev command

thheller20:09:18

but I guess something doesn't like that

Braden Shepherdson20:09:18

with my PR, you can do clojure -M:run, yarn build-hot:cljs and yarn build-hot:js all in separate terminals.

Braden Shepherdson20:09:46

(it's important that the CLJS build finishes before the yarn build-hot:js starts)

Braden Shepherdson20:09:52

huh, that should have worked. I dunno.

thheller20:09:04

ok, if I run that instead it works

thheller20:09:23

ok, I think I found the problem. just not sure what causes it. too tired, will take a look tommorrow

Braden Shepherdson20:09:48

good news! have a good night.

thheller07:09:47

found the cider problem https://github.com/clojure-emacs/cider-nrepl/issues/819, not related to your issue but I'll be looking at that next

thheller09:09:13

webpack 5.85.0 compiled successfully in 72958 ms this isn't normal right?

thheller13:09:52

ok found a bunch of other problems in the REPL code. should all be fixed as of 2.25.7. at least I can't find anything that doesn't work anymore. let me know if you do 😉

Braden Shepherdson13:09:26

I regret to say that the webpack time seems normal enough to me, depending on the speed of the machine and disk. it takes 23300ms on my M1 Mac with an SSD and all the files already in the disk caches because it just ran. just Node things; it touches tens of thousands of JS files in node_modules before it even starts to process the actual input. (the more I work with webpack, and standard JS tooling generally, the more glad I am that it's not my main job.)

thheller13:09:41

seems to be WSL making it so slow, but yeah webpack seems rather slow overall

Braden Shepherdson13:09:45

Bun seems like it'll help, but currently it just generates thousands of errors really fast 😅

thheller13:09:31

yeah the problem is that webpack added so many quirks to the building stuff, that many packages use those quirks and rely on them.

thheller13:09:54

so using anything other than webpack yields a bunch of errors. sometimes I just have to give up in shadow-cljs and direct people to webpack

Braden Shepherdson13:09:09

let me give 2.25.7 a try momentarily.

thheller13:09:31

I had some thoughts on the webpack hot reload stuff too, since webpack just seems to ignore node_modules files updating entirely

thheller13:09:59

so you need to actually restart webpack to get updated CLJS code loaded

thheller13:09:08

at least sometimes, couldn't really figure out a pattern

Braden Shepherdson13:09:23

that's actually still an unexplained difference between yours and mine - it does notice node_modules changes for me.

Braden Shepherdson13:09:32

not sure if that's a file-watcher thing or what

thheller13:09:37

could be WSL related in some way, since it also tried to watch my c:\pagefile.sys for some reason

Braden Shepherdson13:09:47

I wonder if that's a V8 internal thing that the file watcher is wrongly seeing under WSL? many GCs play games with the memory map to put an inaccessible page at the far end of the allocation area. (so they can move the allocation pointer without checking it's still in bounds, and the hardware generates a page fault if they've run out of Eden space. makes the common case a free hardware test instead of a branch instruction.)

Braden Shepherdson13:09:57

it works beautifully! I loaded a test file not already in the classpath, and see all the transitive loads in the console, and the tests run neatly. chefskiss.png!

👍 1
Braden Shepherdson13:09:06

thank you again so much for your superb support! Metabase is an odd case in several ways, but everything is working very nicely now.

thheller13:09:41

happy to fix it, very rare to have access to setups like yours and I want this to work