Fork me on GitHub
#shadow-cljs
<
2021-07-30
>
borkdude09:07:18

@thheller this is not a shadow question per se, so I understand if you want me to ask this in #clojurescript instead, just let me know. This works:

node out/tbd_main.js test.cljs
with the :esm target. But when I install the project with npm install -g and call it from some other directory, then I get:
$ tbd test.cljs
/usr/local/bin/tbd: line 2: import: command not found
/usr/local/bin/tbd: line 3: syntax error near unexpected token `('
/usr/local/bin/tbd: line 3: `const shadow_esm_import = function(x) { return import(x) };'
Should I configure npm to make it work well with the ecmascript module stuff somehow?

borkdude09:07:34

This works fine:

$ node /usr/local/bin/tbd test.cljs

borkdude10:07:30

I guess it needs a shebang or so and not doesn't automatically create this for you

thheller10:07:36

@borkdude look up node and esm. this is a general JS question, nothing specific to CLJS or shadow-cljs. I can't remember the exact rules for node and esm

thheller10:07:06

I mean at this point it really doesn't seem worth the effort to use ESM at all, just more trouble than its worth

borkdude10:07:33

I think the only problem here is the shebang

borkdude10:07:42

but you may be right

borkdude10:07:52

going to back to the :npm-module approach might be better

thheller10:07:11

no, it is not. it is complaining specifically about import. so maybe you need to pass an extra command line argument in the shebang

thheller10:07:44

it just might not default to esm. the problem is that it treats the code as commonjs which doesn't have import

borkdude10:07:16

bash /usr/local/bin/tbd gives exactly the same errors, that's why I think it's a shebang problem

thheller10:07:30

:npm-module is broken and pretty much unfixable in its current state. so don't build anything on top of that

borkdude10:07:23

Adding #!/usr/bin/env node fixed it, but I expect npm to do this for me so it becomes a cross-platform viable way of installing the tool

borkdude10:07:40

anyway, this isn't a shadow problem, I'll bug someone else with it :)

borkdude10:07:07

If you have any other recommendations besides :epm and :npm-module, I'm all ears.

thheller10:07:15

as I said before from the build perspective this is all trivial. there just isn't a target that does this just yet for node. it could easily be built but for that I need more details about how you actually plan on doing any of this

thheller10:07:54

my suggestion is to build all of this completely on top of :node-library and then deal with the code splitting stuff later

thheller10:07:32

it is an optimization after all so you don't need it from the start. if you just keep namespaces separate it'll be trivial to split out later

thheller10:07:02

once I can actually see what you are doing I can make a suggestion and maybe a custom target to do what you need

borkdude10:07:05

great suggestion, I'll try :node-library and will see what needs to be done for code splitting.

thheller10:07:05

I really mean :node-library here as well, not :node-script

thheller10:07:21

use a runner.js like the one I showed you in shadow-cljs

thheller10:07:34

which just locates the proper file and runs that

thheller10:07:43

don't do that from within the CLJS code since it will break in :advanced

borkdude10:07:50

To avoid reading through the wall of text below, I'll start a thread.

borkdude10:07:35

I'm not completely sure what you mean with runner.js, I will have to look into it. I'll try to break down the problems I encounter one by one and keep it focussed. The "hack" of require is done so the script user, who will call this hacked require, can load local npm modules.

thheller10:07:21

in your package.json you'll specify a bin. that should be the runner. as seen here https://github.com/thheller/shadow-cljs/blob/master/packages/shadow-cljs/package.json#L21

thheller10:07:38

that "runner" is then responsible for locating the correct .js file and "running" it

thheller10:07:52

like my variant

thheller10:07:46

the hack then becomes irrelevant as far as I can tell?

thheller10:07:40

so when you run npx shadow-cljs it'll call the runner.js and that will either locate the local node_modules/shadow-cljs install and run that or if not found use a global install

borkdude10:07:43

well, SCI has access to the global object, so it can do interop on "everything" including require. So when someone writes js/require, we look up require from the global object and call it. This is why we need to put an override for require on the global object.

borkdude10:07:06

But the hacked require can then contain similar logic as your runner

thheller10:07:47

I must be missing something because I don't know why you need to provide a custom require

borkdude10:07:24

I was surprised by this too, but this problem is explained here: https://swizec.com/blog/making-a-node-cli-both-global-and-local/

borkdude10:07:54

the problem is that node require doesn't look in the local node_modules when calling a globally installed CLI script

thheller10:07:07

I think we are talking past each other here

thheller10:07:34

the runner.js in shadow-cljs takes care of running either the LOCAL install OR the global install if that is missing

borkdude10:07:15

yes, that is what the hack for require also does

thheller10:07:24

bah no it does not

thheller10:07:40

that overrides that GLOBALLY and does it for every single require

thheller10:07:46

which is just not needed

borkdude10:07:10

it is needed for the js/require calls within the scripts that are interpreted

borkdude10:07:31

so people can install libs locally in their interpreted script dir

borkdude10:07:30

if I don't apply this hack, people will have to write (js/require "./node_modules/my_lib") or so

borkdude10:07:07

but this is just a detail, not the major problem I'm having with shadow-cljs

thheller10:07:14

you are talking about node_modules which already has special rules so the user should NEVER type out node_modules in anything and instead they should be tying (js/require "my_lib")

borkdude10:07:14

we're on a tangent here

thheller10:07:23

which then resolves properly and doesn't require any overrides anywhere

thheller10:07:36

unless I'm completely missing the point on what you are trying to do here

borkdude10:07:39

yes, exactly! but this doesn't work for tbd, see the blog article :)

thheller10:07:23

that blog post is not about the problem you are trying to solve

thheller10:07:37

like I explained with the runner.js logic. it maintains which version of YOUR tool is loaded. either the local or the global. YOUR tool, so tbd. why would you want that for the code the USER is loading?

thheller10:07:00

they can already load local versions by default and you specically do NOT want it to fall back to global versions

borkdude10:07:15

let me put it differently, to make you understand what is not working. and then you can decide if you would solve this different.

borkdude10:07:24

because we are indeed talking past each other

borkdude10:07:34

I will make a branch without the hack

borkdude10:07:21

I must have been confused before since it does work. You are right, once again :)

thheller10:07:55

there are uses for a custom require but never ever mess with the "global" js/require. since that is not actually global but belong to the module you are currently in

thheller10:07:48

and you definitely do not want that to the the reference of all your future requires

borkdude10:07:13

you are right

thheller10:07:13

so if you want to expose a require for sci use the module.createRequire based on the file it is currently eval'ing

thheller10:07:48

also try node --input-type=module in the hashbang stuff

borkdude10:07:45

the hashbang worked just with node, it just didn't have any hashbang, was the problem. But this "missing hashbang" problem isn't present in my non-ESM branch

thheller10:07:45

well yeah this is only for ESM. in case you ever want to "future proof" the thing

thheller10:07:15

never know if node people are actually going to remove support for commonjs at some point

borkdude10:07:43

that's kind of a conflict right: should I continue to work with the ESM stuff to make it "future proof" (with all kinds of async stuff around import) or continue with the :node-library approach

borkdude10:07:43

You could also ask yourself: is node still around in 3 years or will people go all in on deno?

borkdude10:07:07

I'm not that familiar with the node ecosystem that much, it's an experiment for me at this point

thheller10:07:45

I doubt deno will replace node ever but that doesn't mean ESM won't become more adopted generally

👍 2
borkdude10:07:30

I don't think the async import stuff is a dealbreaker, I can make similar restrictions to dynamic requires as CLJS does, just support one top level ns form or require form

thheller10:07:41

commonjs is certainly more convenient in some of these dynamic aspects but the focus has definitely shifted towards ESM

thheller10:07:49

judging from all docs using ESM examples etc

borkdude16:07:22

@thheller I've successfully got your shadow.esm/dynamic-import exposed to SCI so you can write:

(-> (js/import "csv-parse/lib/sync.js")
    (.then (fn [csv]
             (let [csv-parse (.-default csv)]
               (-> (csv-parse "foo,bar,baz\n1,2,3" #js {:columns true})
                   (js->clj :keywordize-keys true)
                   prn)))))
I think the (require '["csv-parse/lib/sync.js" :default csv-parse]) can be supported instead, but I wonder why I needed to write .js... any idea here? Should I wrap this function and automatically add this? That might be a bad idea. As (js/import "fs") works and (js/import "fs.js") for example doesn't.

borkdude16:07:51

Should I just leave this alone in the ns or require form and just let the user figure it out?

thheller18:07:55

why not just (js/import "csv-parse")? specifying the extension when referencing a file is node stuff. I think thats a new requirement to esm, should have been that way always tbh

borkdude18:07:14

does node support it like this, as in (esm/dynamic-import "cvs-parse") ?

borkdude18:07:40

node that this lib/sync is a module within that package

thheller18:07:43

I'd assume so? don't know the lib

borkdude18:07:45

part of their api

borkdude18:07:40

@thheller Now got this working as a "tbd" script:

(ns foo
  (:require ["fs" :as fs]
            ["csv-parse/lib/sync.js" :default csv-parse]
            [reagent.core :as r]
            #_[reagent.dom.server :as rds]))

(println (str (.readFileSync fs "test.cljs")))

(prn :hello-the-end)

(prn (csv-parse "foo,bar"))

#_(prn (rds/render-to-string [:div [:p "hello"]]))

👍 2
borkdude18:07:00

not loading reagent saves a bit of time, like 20ms or so. it's not a lot, but it shows that, architecturally, it's now possible to add more and more built-in libs, without impacting startup if you don't load all of those

borkdude19:07:11

Now I remember what the "hack" was for.

borkdude19:07:32

If you have a local file relative to the script, e.g. foo.js and you try to load it with:

(ns script (:require ["./foo"]))
This doesn't work

thheller20:07:19

again DO NOT USE THAT HACK!

thheller20:07:38

use the module.createRequire if you must have a custom require (pretty sure there is a import equivalent)

borkdude20:07:12

note that this doesn't hack import globally though

borkdude20:07:41

but I'll look into it :)

borkdude10:07:10

@thheller I'm trying to do the createRequire thing. For:

import { createRequire } from 'module';
I'm writing: (:require ["module" :refer [createRequire]]]) but then I get:
The required JS dependency "module" is not available, it was required by "nodashka/core.cljs".

Dependency Trace:
	nodashka/core.cljs
Not sure if this is a correct message, since module is built into nodeJS, I believe?

borkdude10:07:46

When I try:

(def createRequire (.-createRequire js/module))
(def require* (createRequire js/module.meta.url))
I get:
ReferenceError: module is not defined in ES module scope

borkdude11:07:32

I have removed the hack as you suggested, but when I install nodashka (current working name) globally using npm install -g nodashka and then have a local node_modules with e.g. the ink dependency, it can't find it. That is the problem I'm trying to solve.

borkdude11:07:41

When you npm install -g ink then it does work. Perhaps this should just be how it works...?

borkdude11:07:53

I guess this is perhaps just the way it should be. Mixing global deps from nodashka itself with local deps would perhaps be weird.

thheller16:07:14

if you are still using the esm target you need to add "module" to the :keep-as-import set in your build config

thheller16:07:28

otherwise it'll try to bundle it but since its a built-in node package it can't do that

borkdude16:07:03

now I remember from the fs thing, thank you

borkdude16:07:39

I think I'm able to get "require" back through this createRequire thing even in ESM. I thought ESM would prevent me from doing so and all requires are now async. But I could revert that.

borkdude16:07:59

On the other hand, maybe for the future, one could require from some http address and then it would have be async anyway

thheller16:07:53

I'm not sure how this all looks for pure esm. the createRequire looks commonjs-ish

borkdude16:07:04

I'll give it a try. I'm still confused about having nbb (yes, changed the name) as a global tool from npm and using it with node_modules in a local script dir. Do you think this is possible, or should a global tool always use the global npm deps?

thheller16:07:32

guess its ok to use require that way?

thheller16:07:33

btw you should maybe try to not use :default. use the official ["thing$default" :as x] instead of ["thing" :default x]

borkdude16:07:28

oh I didn't know about that

borkdude16:07:38

it does ring a bell, it was a recent addition right

thheller16:07:07

quite useful addition. very handy if you have a lot of nested properties

👍 2
borkdude18:07:44

How do I write import.meta.url in CLJS/shadow?

borkdude18:07:04

I tried with (.. js/import -meta -url), and even js* but both fail

thheller18:07:25

fail in release you mean?

borkdude18:07:51

in non-release it did work

thheller18:07:26

yeah the closure-compiler wants to process it statically

thheller18:07:46

so using it dynamically requires some trickery, as seen in shadow.esm/dynamic-import

thheller18:07:10

so js/shadow_esm_import.meta.url should work

borkdude18:07:08

hmm. TypeError: Cannot read property 'url' of undefined

thheller18:07:14

check if js/shadow_esm_import is actually set

thheller18:07:21

can't remember how I did this exactly

thheller18:07:39

if all else fails (js/eval "import.meta.url")

borkdude18:07:10

I think it's actually set, else meta would have been null already

borkdude18:07:27

trying eval now

thheller18:07:59

meta is undefined thats why you get url of undefined

borkdude18:07:21

yes, sorry, else I would have gotten an error about meta I mean: meta of undefined

borkdude18:07:51

with js/eval:

import.meta.url
       ^^^^

SyntaxError: Cannot use 'import.meta' outside a module

thheller18:07:02

ah right it wraps import since closure otherwise complains

thheller18:07:20

well yes, as always import is only available in modules

borkdude18:07:20

perhaps you can put meta on that function as well? :-D

thheller18:07:43

that wouldn't do anything to fix your problem. what are you trying to do exactly?

borkdude18:07:02

I was trying to follow your advice of using createRequire

borkdude18:07:22

(def require* (createRequire (js/eval "import.meta.url") #_(.. js/shadow_esm_import.meta.url -meta -url)))

thheller18:07:35

ehm thats wrong

thheller18:07:53

you want to create a require that uses the source of the current file as a reference

thheller18:07:26

so not the file you are in but the file you are eval'ing

borkdude18:07:04

oh that you mean, so cwd() basically

thheller18:07:45

the file you are evaling

thheller18:07:54

tbd some-script.cljs

thheller18:07:58

so some-script.cljs

thheller18:07:11

but full path resolved using node path/resolve

borkdude18:07:40

ok, this sounds promising... trying

borkdude18:07:32

now I finally get what you were getting at

thheller18:07:12

dont' forget to add "path" to :keep-as-import when you import it 😉

borkdude18:07:42

I have much to learn in this node ecosystem, I'm glad you're patient ;)

thheller18:07:22

its not that complicated but helps to understand it. saves you from trying weird hacks 😉

thheller18:07:19

require in node always has a "context". ie. the module you are currently in so it knows how to follow relative paths

thheller18:07:33

and follow the node resolve rules of finding node_modules

borkdude18:07:18

great, now my requires look like this again:

(ns script
  (:require
   ["csv-parse/lib/sync" :as csv-parse]
   ["fs" :as fs]
   ["shelljs" :as sh]))
Instead of (which was implemented via js/import):
(ns script
  (:require
   ["csv-parse/lib/sync.js" :default csv-parse]
   ["fs" :as fs]
   ["shelljs" :default sh]))

borkdude18:07:34

I'll move everything back to require, much more node-ish

👍 2
thheller18:07:09

well maybe add ["csv-parse/lib/sync.js$default" :as csv-parse]

thheller18:07:28

has the added benefit of csv-parse/foo working since its an alias

borkdude18:07:51

yeah, I will do that after I get this sorted

thheller18:07:56

probably shouldn't adopt :default at this point since it was rejected from CLJS proper

borkdude19:07:11

yeah, it was my ignorance :)

borkdude19:07:39

now people can also just use js/require again like they are used to

borkdude19:07:20

I'll probably move this $ back to SCI proper so it can work for all CLJS applications using it

borkdude19:07:53

Hmm, it seems I'm still stuck in the async import stuff. I did manage to create require which resolves according to the file but you cannot require an ES Module with require, so I'll have to keep doing that using dynamic import.

borkdude19:07:06

(this is for the lazy loading of optional modules at runtime)

borkdude19:07:50

and I don't know if there is a createImport instead of createRequire

borkdude19:07:53

It seems for the import stuff to work, I need access to:

import.meta.resolve also accepts a second argument which is the parent module from which to resolve from:

await import.meta.resolve('./dep', import.meta.url);

borkdude20:07:36

I tried to apply a similar trick to yours, but it didn't work.

"globalThis.shadow_esm_resolve = function(x,y) { return import.meta.resolve(x,y); }"

borkdude20:07:47

TypeError: (intermediate value).resolve is not a function

borkdude20:07:09

I admit fully that I didn't know what I was doing, but it was worth a shot ;)

borkdude21:07:04

But I think, either the above import.meta.resolve should be used, or, I should try something else than these ESM modules, or I should write my own resolver...

borkdude21:07:59

Perhaps continueing with :node-library and then making the code splitting work

borkdude21:07:03

So, I've also created a branch node-library here: https://github.com/borkdude/nbb/tree/node-library The compilation works nicely. It's just that I want code splitting so I can load "splits" dynamically, as needed.

borkdude21:07:07

So perhaps we can look into making this works. It seems a lot "easier" from the nodeJS side than this ESM stuff

borkdude09:08:31

@thheller Good news. I was able to use the createRequire stuff while also loading my own ESM modules async. So now the programs look "node-ish" , while still leveraring your ESM target with module support.

borkdude09:08:54

Once you support modules in the node-library target I'll likely move to that, but for now, we're good :-D

borkdude09:08:03

Also the local npm library require stuff works as expected now.

borkdude09:08:00

I will keep an eye on shadow for that feature, but I'm out of the woods now for my own needs

borkdude10:08:33

@thheller Btw, the way of resolving from the script file root got me thinking. I don't think the clojure CLI does this, does it.

borkdude10:08:05

e.g. foo/deps.edn + foo/script.clj + clojure foo/script.clj will not use foo/deps.edn but only the local deps.edn

borkdude10:08:15

I think it could be useful for normal babashka if it did...

iGEL10:07:10

Hello. I got one of these newbie questions: Want to run some cljs tests, but I guess my project layout is causing some problem:

/webapp/cljs/admin/shadow-cljs.edn
            /tools/src/money.cljs
                  /test/money_test.cljs
            /output/
            /customer ; Separate cljs project also using tools, but not build by this shadow-cljs process
Here the relevant parts of the shadow-cljs.edn
{:source-paths ["src"
                "../tools/src"
                "../tools/test"]
 :builds {:test {:target :node-test
                 :output-to "../output/node-tests.js"}}}
When I compile & run the tests (`pwd -> /webapp/cljs/admin`, I run node_modules/.bin/shadow-cljs compile test && node ../output/node-tests.js), I get this output:
shadow-cljs - config: /webapp/cljs/admin/shadow-cljs.edn
shadow-cljs - socket connect failed, server process dead?
[:test] Compiling ...
[:test] Build completed. (51 files, 1 compiled, 0 warnings, 1.92s)
no "source-map-support" (run "npm install source-map-support --save-dev" to get it)
fs.js:114
    throw err;
    ^

Error: ENOENT: no such file or directory, open '/webapp/.shadow-cljs/builds/test/dev/out/cljs-runtime/goog.debug.error.js'
    at Object.openSync (fs.js:443:3)
    at Object.readFileSync (fs.js:343:35)
    at global.SHADOW_IMPORT (/webapp/cljs/output/node-tests.js:52:15)
    at /webapp/cljs/output/node-tests.js:1524:1
    at Object.<anonymous> (/webapp/cljs/output/node-tests.js:1575:3)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
The file exists, but in the wrong location:
ls /webapp/{,cljs/admin/}.shadow-cljs/builds/test/dev/out/cljs-runtime/goog.debug.error.js
ls: cannot access '/webapp/.shadow-cljs/builds/test/dev/out/cljs-runtime/goog.debug.error.js': No such file or directory
/webapp/cljs/admin/.shadow-cljs/builds/test/dev/out/cljs-runtime/goog.debug.error.js
My node version is v10.24.0 from the Debian Docker image I use and shadow-cljs is 2.15.2.

thheller10:07:51

@igel don't use ../ in :output-to. that is supposed to be a directory in the project.

iGEL10:07:12

ok, I'll try. thx

thheller10:07:35

if you want it outside the project dir setting :output-dir "../output" in addition to :output-to might work

iGEL10:07:13

It does. Thank you 👍

kennytilton10:07:52

We are building on https://github.com/PEZ/rn-rf-shadow and have just added [cljsjs/mqtt "2.13.0-0"] as a dependency. In the code, we require [cljs.mqtt]. Problem: error from the shadow build app: "The required namespace "cljs.mqtt" is not available, it was required by "example/app.cljs"." This all works fine in a web app we have, so I am guessing this is something to do with RN. Will shifting to interop with an RN MQTT NPM package help? We are trying: https://www.npmjs.com/package/react-native-paho-mqtt in the meantime.

thheller10:07:31

@hiskennyness shadow-cljs does not support CLJSJS packages, just use the npm package directly

thheller10:07:38

I'm guessing the webapp you have isn't built with shadow-cljs, otherwise you'd have the same issue there

kennytilton10:07:43

Doh! Forgot to mention we are using figwheel on the web app.

iGEL12:07:42

I'm getting an error with a namespace in a test build, but not the browser build, and I don't understand why. Here the relevant parts I guess: /shadow-cljs.edn:

{:source-paths ["src" "test"]
 :builds {:credit-assessment {:target :browser ;; Works!
                              :modules {:credit-assessment {:entries [credit-assessment.core]}}
                              :output-dir "../output/"
                              :asset-path "/cljs/output"}
          :test {:target :node-test ;; Doesn't work :'(
                 :output-to "../output/node-tests.js"
                 :output-dir "../output"}}}
/src/credit_assessment/core.cljs (requires credit_assessment.criteria) /src/credit_assessment/criteria.cljs:
(ns credit-assessment.criteria)
/test/credit_assessment/criteria_test.cljs:
(ns credit-assessment.criteria-test
 (:require
  [clojure.test :refer [deftest testing is are]]
  [credit-assessment.criteria :as creteria]))
This works for the credit-assessment build, but for the test build, I get this:
[:test] Compiling ...
------ ERROR -------------------------------------------------------------------
 File: /test/credit_assessment/criteria_test.cljs:1:2
--------------------------------------------------------------------------------

   1 | (ns credit-assessment.criteria-test
--------^-----------------------------------------------------------------------
An error occurred while generating code for the form.
ExceptionInfo: no source by provide: credit-assessment.criteria

thheller12:07:11

hmm not sure. FWIW you must not use the same :output-dir for multiple builds

thheller12:07:17

but thats unlikely to affect this

iGEL12:07:49

I will try it anyway

iGEL12:07:49

As you said, doesn't seems to be the issue

jmckitrick20:07:01

Hi all, I’m trying to build a shadow-cljs project in a luminus template, and then separate the build as I’ve been instructed here.

jmckitrick20:07:07

However, here is what happens:

thheller20:07:03

get rid of the lein

2
jmckitrick20:07:37

So does this mean the luminus template has been updated to run shadow separately rather than as a lein task?

thheller20:07:30

I guess? I have never used that template so no clue what it looks like