Fork me on GitHub
#shadow-cljs
<
2023-10-10
>
selahb18:10:07

Request for guidance: • I'm trying to develop a cljs plugin for a (closed-source) Electron app, Obsidian.md, and would like REPL support • The app requires plugins to be bundled into a single JS file, which only happens with Shadow on release as far as I can tell. • However, Shadow only includes JS runtime repl code on dev builds, which makes sense, but doesn't help in my case One thought is to somehow explicitly include the node repl client code in the plugin (as would be done automatically for a dev build), build a release, and run that build to connect to shadow server and provide the JS runtime. 1. Does this sound like a viable solution, or is there a potentially better approach? 2. I'm looking at shadow/cljs/devtools/client/node.cljs , but it doesn't look designed for public consumption. Is there a clean way to start the node repl client in a non-dev build? Thanks!

thheller18:10:57

can you point me to some docs that describe how the plugins are supposed to look?

thheller18:10:11

it is not possible to get a repl into a release build

selahb18:10:22

I was afraid of that. FWIW, I was able to start a sci-powered node repl from within the plugin (similar to how vs code Joyride does it), but this isn't a build-aware repl and doesn't work well with Cider. Here is the Typescript sample plugin repo: https://github.com/obsidianmd/obsidian-sample-plugin It includes an esbuild config that bundles everything into a single .js file.

thheller18:10:06

and what does your cljs setup look like?

selahb18:10:46

{:deps            true
 :nrepl            {:port 8702}
 :compiler-options {:infer-externs           :auto
                    :output-feature-set      :es-next
                    :source-map              true
                    :source-map-detail-level :all
                    :warnings                {:fn-deprecated false}}
 :builds
 {:plugin    {:target           :node-library
              ;; :asset-path    ""
              :output-dir       "cljs-out"
              :output-to        "saber.js"
              :exports          {:evalCljs   saber.core/eval-cljs
                                 :nreplStart saber.nrepl/start-server+
                                 :nreplStop  saber.nrepl/stop-server+
                                 :loadFile   saber.core/load-file
                                 :main       saber.core/main
                                 ;; :nrepl saber.nrepl-server/start
                                 }
              :compiler-options {:optimizations :none
                                 :externs       ["datascript/externs.js"]}
              :modules          {:saber {:entries [saber.core]}}
              ;; :build-hooks   [(portal.shadow.remote/hook)]
              :devtools         {
                                 ;; :preloads    [devtools.preload]
                                 ;; :after-load  core/reload
                                 :repl-pprint true
                                 :watch-dir   "cljs-out"}
              :release          {:compiler-options {:optimizations :simple}
                                 :externs          "datascript/externs.js"}}}}

selahb18:10:12

shadow-cljs included in deps.edn

thheller18:10:19

are there any actual docs? the sample repo doesn't answer some essential questions

thheller18:10:37

I mean is it an actual node runtime or is it a browser iframe or something?

selahb18:10:54

It is an actual node runtime, and node api is accessible to plugins

selahb18:10:13

As is the full electron api

thheller08:10:47

I looked at this for a bit. there is no easy way to make this work. the entire thing is locked down a little bit too much.

thheller08:10:13

the shadow-cljs node repl implementation requires access to the disk, but the plugin stuff isn't allowed to

thheller08:10:02

do you explicitely need a REPL into the obsidian process, or do you just want a REPL?

selahb15:10:31

Thanks for looking into it—I really appreciate it. The security model is a bit ... weird. SHADOW_IMPORT statements are not allowed (the plugin needs everything concatenated into a single file), but then you can freely use the "fs" module to access the file system. ¯\(ツ)/¯ What I have been able to do is: 1. Connect to a node REPL on an independent node process 2. Start a node nREPL from within the plugin using SCI for evaluation and connect to it using Calva (no Cider without piggyback) Both of these give me a partially functional repl, but neither is perfect. Honestly, it's the browser side of Electron that I need access to more than the Node side. Plugins are js modules, not HTML pages, but maybe if I inject some JS into the DOM on plugin initialization, I can get a functional browser repl...

Chris McCormick01:10:47

Not sure if this is relevant but I did something similar writing a plugin for Joplin (an Obsidian alternative) which also uses Electron. I had to use build target :browser to get it to work properly.

thheller07:10:41

FWIW I got it working

thheller08:10:10

got curious how the plugin system works. turns out it isn't node. just the usual electron renderer process (i.e. a browser)

thheller08:10:51

so I just made a regular main.js that gets loaded

thheller08:10:55

const { Plugin } = require("obsidian");

exports.default = class MyPlugin extends Plugin {
   load() {
     console.log("load plugin");
     return import("");
   }

   unload() {
     console.log("unload plugin");
   }
};

thheller08:10:03

and then just loads the regular :target :esm build

thheller08:10:59

seems to work just fine, but will require some more adjustments if you want access to the obsidian (or other provided ones)

thheller08:10:08

but repl/hot-reload work just fine

thheller08:10:53

this is really the only important bit to know, then you can hack everything else.

function(e, t) {
                                return window.eval("(function anonymous(require,module,exports){".concat(e, "\n})\n//# sourceURL=").concat(t, "\n"))
                            }(i, "plugin:" + encodeURIComponent(e))(o, s, a),

thheller08:10:12

so it just takes whatever is in main.js, wraps it in a function, evals and calls it

thheller08:10:26

of course you wouldn't use that setup for anything "production", but for development its fine

selahb13:10:41

Awesome, thanks! I'll give it a try.

selahb17:10:32

FYI, this works great. Thanks for your guidance! I added :js-options {:keep-native-requires true} to support node native modules. I did run into what I think is a bug when trying to connect to the REPL: The browser (electron) side of the connection attempts to connect to ... even though I have specified :http {:host "localhost"} . For now, I have just manually overridden this, but can dig into why this is happening assuming I'm not just missing some necessary configuration.

thheller17:10:26

ah sorry, forgot to mention that. use :devtools {:use-document-host false} in the build config. it defaults to using the document.host property, which I guess defaults to obsidian.md

1