This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
@thheller I’m working on an interesting application of shadow-cljs: a wasm-based server-side sandboxed runtime. Being a Clojure shop, I would like to include a Clojure or Clojurescript dialect in the offered languages. I got a POC running with the latter using shadow-cljs, albeit awkwardly. I’m writing to ask if there is a better way to do what I am doing or if there is interest in making the general options more first-class within shadow-cljs. I will admit right up front that I am not a javascript expert, so I may have very well missed some opportunities to do what I am about to explain in a more shadow-cljs direct/supported way. But in a nutshell, my flow is 1. Create a nodejs compatible script with shadow-cljs by compiling cljs to :target :node-script. 2. Manually stripping the shell preamble in the generated :node-script output, because rollup (see next step) doesn’t like this. 3. Running rollup (https://www.npmjs.com/package/rollup) against the shadow-cljs generated code using the node-resolve/node-builtins/node-globals rollup plugins to create a .mjs ESM module. 4. Running javy (https://github.com/bytecodealliance/javy) to bundle the .mjs ESM along with quickjs runtime and compile them to a .wasm target that can be directly run by wasm runtimes such as wasmtime or wasmedge, similar to how one might run the original :nodescript via nodejs. A few thoughts to consider: a. an option for the :node-script to omit the preamble (assuming it doesn’t already exist). b. a target that combines the :node-script and rollup steps (e.g. “:node-rollup”?) to produce a ESM compatible output might be interesting c. a first-class :wasm target that combines all of the steps might also be interesting.
I mean you maybe could develop against node and then just use this as a deployment target
im basically trying to provide a lambda-function like substrate in the backend, so sandboxing is critical as the code will be untrusted
i would guess the javy limitations are more to do with quickjs vs a full v8 interpreter
so far, from a POC perspective, the stack looks promising in terms of bringing clojure(script) to the mix
release
builds will likely just work, dev builds will probably never work, so no watch/compile
yeah…i was prototyping with release…didnt even try watch/compile as the rollup wouldve been ugly at best
basically just compiles everything and puts it into a single file. completely without anything runtime specific
I would like to retain the ability to bring in npm modules…is that precluded with that target?
> I have not looked at javy, so dunno what its limitation for the packaged JS are? One thing I might not have been clear about: This actually all works (at least for my basic POC cljs code, so far). Which is very cool and a testament to shadow-cljs
I think you were getting at the ability for javy to support hot-reload, etc, but I just wanted to emphasize that at least as a release target flow, everything I described worked, which I am thrilled about
I mean for example in react-native the hermes runtime doesn't support const
, so that has to be transpiled to var
also a while back people got excited about the graaljs
runtime, which works but is also a quite limited runtime. so in the end doing something practical with that becomes annoying
yeah as far as I understand the docs the JS is left as is and just executed via quickjs
> QuickJS is a small and embeddable Javascript engine. It supports the ES2023 specification including modules, asynchronous generators, proxies and BigInt.
In any case, my customers just need to do very simple lambda-function like things to do transformations, so this might be good enough to offer a clojure dialect but not enough for a major app
actually https://bellard.org/quickjs/quickjs.html#Standard-library this looks quite capable
busy with other stuff currently, but I'm intrigued enough to check it out sometime soon
@thheller so I tried the :esm option, and I get something that compiles but when I run it, nothing happens. At a high level, I understand that the issue is likely that nothing understands where the application entry point is when I am only compiling it as an ESM module. I tried to hack it with an init-fn but still no dice. I am a bit out of my depth on the js side, but perhaps you have an idea
{:builds {:app {:target :esm
:output-dir "target"
:init-fn shadow-wasm.core/main
:modules {:app {:exports {default shadow-wasm.core/main}}}}}
:lein true}
(ns shadow-wasm.core)
(defn hello []
(println "Hello, World"))
(defn main []
(hello))
12:08 $ shadow-cljs release app
shadow-cljs - config: /Users/ghaskins/sandbox/wasm/sci/shadow-cljs.edn
shadow-cljs - running: lein run -m shadow.cljs.devtools.cli --npm release app
[:app] Compiling ...
[:app] Build completed. (45 files, 0 compiled, 0 warnings, 4.67s)
(default ) ✔ ~/sandbox/wasm/sci [master L|⚑ 1]
12:09 $ javy compile target/app.js -o target/app.wasm
(default ) ✔ ~/sandbox/wasm/sci [master L|⚑ 1]
12:09 $ time wasmtime target/app.wasm
real 0m0.458s
user 0m1.833s
sys 0m0.119s
{:builds {:app {:target :esm
:output-dir "target"
:runtime :custom
:modules {:app {:init-fn shadow-wasm.core/main}}}}
:lein true}
I mean technically it was a correct config, just not doing what you expected probably
basically the top level :init-fn
had no effect, and you just got afile with a default export
the :runtime :custom
is just there to stop some browser-specific stuff being injects since :browser
is the default
$ time wasmtime target/app.wasm
Hello, World
real 0m0.451s
user 0m1.829s
sys 0m0.111s
but to your earlier suggestion: in this new world its only shadow :esm and javy, no more rollup or the node preamble trickery
I did some brief quickjs tests myself. the runtime is basic JS but has no support for websockets or fetch
there are some built-in modules, since shadow-cljs won't be able to bundle them you probably want to set :js-options {:keep-as-import #{"os" "std"}}
in the build config
oh, that will be interesting…Im still new in the wasm world, too, so its not clear what ultimate limitations I may face
{:builds {:app {:target :esm
:output-dir "target"
:runtime :custom
:js-options {:keep-as-import #{"os" "std"}}
:modules {:app {:init-fn shadow-wasm.core/main}}}}
:lein true}
if you feel like doing some benchmarks I'd be interested to see some numbers comparing to node/browser
if you have pointers as to what you’d like to see benchmarked, I can try to give it a whirl
$ time wasmtime target/app.wasm
Hello, World
real 0m0.052s
user 0m0.033s
sys 0m0.016s
FWIW, there is anecdotal evidence that fetch is somehow supported on top of quickjs at least when running on wasmedge: https://wasmedge.org/docs/develop/javascript/networking#fetch-client
I just tried this too:
diff --git a/project.clj b/project.clj
index 088703e..4863aae 100644
--- a/project.clj
+++ b/project.clj
@@ -4,5 +4,6 @@
[org.clojure/clojurescript "1.11.132"
:exclusions [com.google.javascript/closure-compiler-unshaded
org.clojure/google-closure-library]]
- [thheller/shadow-cljs "2.27.1"]]
+ [thheller/shadow-cljs "2.27.1"]
+ [funcool/httpurr "2.0.0"]]
:source-paths ["src"])
diff --git a/src/shadow_wasm/core.cljs b/src/shadow_wasm/core.cljs
index f1319a2..8757121 100644
--- a/src/shadow_wasm/core.cljs
+++ b/src/shadow_wasm/core.cljs
@@ -1,7 +1,9 @@
-(ns shadow-wasm.core)
+(ns shadow-wasm.core
+ (:require [httpurr.client.xhr :as http]))
(defn hello []
- (println "Hello, World"))
+ (println "Hello, World")
+ (http/get ""))
(defn main []
(hello))
\ No newline at end of file
$ time wasmtime target/app.wasm
Hello, World
Error while running JS: Uncaught ReferenceError: 'XMLHttpRequest' is not defined
at Wg (function.mjs)
at <anonymous> (function.mjs:279)
at <anonymous> (function.mjs:286)
at fg (function.mjs:97)
at <anonymous> (function.mjs)
at <anonymous> (function.mjs)
at d (function.mjs)
at e (function.mjs)
at call (native)
at h (function.mjs)
at <anonymous> (function.mjs:290)
Error: failed to run main module `target/app.wasm`
Caused by:
0: failed to invoke command default
1: error while executing at wasm backtrace:
0: 0x61516 - <unknown>!<wasm function 109>
1: 0x739d0 - <unknown>!<wasm function 169>
2: 0xbf6de - <unknown>!<wasm function 1050>
2: wasm trap: wasm `unreachable` instruction executed
real 0m0.469s
user 0m1.849s
sys 0m0.119s
This is something that perhaps you understand: So I added this
diff --git a/src/shadow_wasm/core.cljs b/src/shadow_wasm/core.cljs
index f1319a2..45e4fce 100644
--- a/src/shadow_wasm/core.cljs
+++ b/src/shadow_wasm/core.cljs
@@ -1,7 +1,8 @@
-(ns shadow-wasm.core)
+(ns shadow-wasm.core
+ (:require ["http" :refer (fetch)]))
(defn hello []
- (println "Hello, World"))
+ (println "Hello, World: " fetch))
(defn main []
(hello))
but I get this
$ shadow-cljs release app
shadow-cljs - config: /Users/ghaskins/sandbox/wasm/sci/shadow-cljs.edn
shadow-cljs - running: lein run -m shadow.cljs.devtools.cli --npm release app
[:app] Compiling ...
The required JS dependency "http" is not available, it was required by "shadow_wasm/core.cljs".
Dependency Trace:
shadow_wasm/core.cljs
Searched for npm packages in:
/Users/ghaskins/sandbox/wasm/sci/node_modules
http is part of the node-libs-browser polyfill package to provide node-native package support
for none-node builds. You should install shadow-cljs in your project to provide that dependency.
npm install --save-dev shadow-cljs
See:
$ cat package.json
{
"private": true,
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"shadow-cljs": "2.27.1"
}
}
I think part of the story here is that it looks like wasmedge might supply a forked version of quickjs
if its also built-in for javy you can use :keep-as-import
to tell shadow not to bundle it, and instead load it at runtime
I think the deal is that wasmedge has a modified quickjs which supplies certain things like fetch
I should tweak that error message I guess, as it does not apply to :target :esm
. only :browser
builds
which means I probably cant use javy, but I can probably still use the shadow :esm on top
but I might need shadow to basically build as if its :node-script but bundled as if its :esm / rollup
literally the problem is that shadow-cljs by default for :esm tries to bundle ALL dependencies
I guess the only difference is that you are using rollup with node-builtins
, which basically tells rollup to not bundle node built-in
an alternate is adding :js-options {:js-provider :import}
, that will make shadow-cljs NOT bundle anything
which you should do if you think about using rollup at all, as shadow partially bundling some stuff and then rollup the rest doesn't make sense
:node-script
is very specifically targetting node
, hence the name. it does not seems to be the correct fit here.
> I don’t know why you think rollup does anything useful here? Sorry, I just meant “need an ESM module, similar to how shadow :esm or rollup provide”
IIU, I think what I need to do is basically have shadow bundle anything that is not a node built-in, and tell it to skip things that are node builtins, and they will be provided by the wasmedge variant of quckjs
yeah, it is best to not bundle anything the runtime provides. as you'd likely just get a worse variant from npm
$ git diff
diff --git a/shadow-cljs.edn b/shadow-cljs.edn
index a50167c..4a8cc31 100644
--- a/shadow-cljs.edn
+++ b/shadow-cljs.edn
@@ -1,6 +1,6 @@
{:builds {:app {:target :esm
:output-dir "target"
:runtime :custom
- :js-options {:keep-as-import #{"os" "std"}}
+ :js-options {:keep-as-import #{"os" "std" "http"}}
:modules {:app {:init-fn shadow-wasm.core/main}}}}
:lein true}
diff --git a/src/shadow_wasm/core.cljs b/src/shadow_wasm/core.cljs
index f1319a2..c9861a7 100644
--- a/src/shadow_wasm/core.cljs
+++ b/src/shadow_wasm/core.cljs
@@ -1,6 +1,8 @@
-(ns shadow-wasm.core)
+(ns shadow-wasm.core
+ (:require ["http" :refer (fetch)]))
(defn hello []
+ (js/console.log fetch)
(println "Hello, World"))
(defn main []
$ wasmedge --dir .:. wasmedge_quickjs.wasm target/app.js
ReferenceError: could not load module filename 'http'
heh…apparently. Im basically trying to create the cljs equivalent to https://github.com/second-state/wasmedge-quickjs/blob/main/example_js/wasi_http_fetch.js
in the docs it says Install WasmEdge-QuickJS.
did you do that? seems to be an optional addon
you can just create a .js file with
import { fetch } from 'http';
console.log(fetch)
its not just that I need the wasmedge-quickjs, but I also need to grant the runtime the modules that it refers to
I tested quickjs with a compile/watch build and they both worked. just had to set :devtools {:enabled false}
there is no live-reload or REPL but still better than waiting for release
build all the time
its possible with stock quickjs as well using the std
package, just not using websockets
urlGet(url, options = undefined)
Download url using the curl command line utility. options is an optional object containing the following optional properties: