31 December 2020

Clojure CLI tools - To jack-in or connect, that is the question

clojure-cli logo

To jack-in or connect, that is the question. Whether it is more effective to relying on the editor to auto-inject the required dependencies for every workflow, or assemble your own aliases them include any editor - William "Hacker" Shakespear

A REPL is the central part of Clojure development. For the full workflow, an editor connects to the REPL to evaluate code and show instant results.

The developer can be in control of how the REPL starts, managing libraries and tools used alongside the REPL. Or the editor can automate the process and control how the REPL is started.

Cider, Calva and Conjure are Clojure development environments for editors that support this approach. Lets discuss the value and constraints for each approach, particularly in respect to the Clojure CLI tools.

Connect

Using the connect approach gives you full control over how the REPL is run and what other tools and dependencies to include. There is also no concern about aliases clashing with any automatically injected configuration by the editor.

Running a process outside of an editor ensures that process is not affected by the editor being closed or crashing, making for a more robust REPL.

An external REPL can also be run in a local container (Docker, ContainerD, VirtualBox) or a remote container / server in a public / private cloud, company data center.

When editors connect to a REPL they are connecting to a server process, such as nREPL server (the most common), Socket REPL server or pREPL. For nREPL this requires configuration that will include the library and namespace to run the nREPL server. Socket REPL is part of the Clojure standard library, so does not require a library, only configuration to start the socket server.

The connect approach requires library versions to be kept up to date with those used by the chosen editor. practicalli/clojure-deps-edn user level configuration includes the :project/outdated alias that will report current and new versions of libraries available.

Running more than just a REPL (eg. a data visualization tool) is a simple matter of ensuring the aliases are included in the appropriate order, as only the :main-opts from the last alias in the chain will be run.

As you control the options used with the clojure command, you can craft your own aliases to meet the needs of a specific project in deps.edn file. Its also recommended to take common configuration and craft aliases in ~/.clojure/deps.edn that work over all projects to save duplication and simplify project deps.edn configuration. For example, when a user level configurations include aliases for a specific tools or REPL communication protocols then they can be used over and over again for each project under development.

So the connect approach provides a clean and flexible approach to running a REPL, optionally with additional tools, in various local and remote environments and without relying on hidden magic from an editor that could cause issues.

jack-in

When using jack-in the editor configures how the REPL is started, by defining the clojure command line that will run externally. Once the server process for the REPL has started, the editor automatically connects. The editor can then be used to evaluate Clojure code in the REPL, directly from the source code file. This approach minimizes the setup required by the developer to get started.

Depending on how the editor assembles the clojure command line, it can cause issues running the REPL process when trying to use additional aliases, especially when alias design is conflated by including unnecessary :main-opts configuration.

As the editor is injecting configuration, it is important to understand how each editor being used is creating the clojure command line, including any limits or caveats in using this approach.

Configuring jack-in with :extra-paths and :extra-deps is simple. However, adding in tools such as data browsers or anything with a :main-opts can conflict with the automatic jack-in process.

Cider jack-in

Cider uses the following form when assembling the jack-in command

[nREPL] Starting server via /usr/local/bin/clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "0.8.3"} cider/cider-nrepl {:mvn/version "0.25.5"}}}' -m nrepl.cmdline --middleware '["cider.nrepl/cider-middleware"]'

Aliases can be included via an Emacs variable called cider-clojure-cli-global-options inside a .dir-locals.el file. Alias names can come from the user level configuration and the project deps.edn configuration.

((clojure-mode . ((cider-preferred-build-tool . clojure-cli)
                  (cider-clojure-cli-global-options . "-M:env/dev:env/test"))))

cider then builds a command line including the alias

When including an alias in the global-options that defines a :main-opts a conflict will arise as cider will build a command line with more than one -main function to run. This may prevent the jack-in process from working. A simple solution to this is to include an alias that has the same configuration that cider auto-injects. practicalli/clojure-deps-edn configuration contains the :middleware/cider-clj alias that contains the cider auto-injected configuration.

Running jack-in with global-opts set to -M:alias:alias-with-main:middleware/clojure-clj will ensure that the clojure command calls the -main to run the REPL regardless of :main-opts defined in the other aliases. The clojure command will use the :main-opts only from the last alias in the chain.

[nREPL] Starting server via /usr/local/bin/clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "0.8.3"} cider/cider-nrepl {:mvn/version "0.25.5"}}}' -M:alias:alias-with-main:middleware/clojure-clj -m nrepl.cmdline --middleware '["cider.nrepl/cider-middleware"]'

It is possible to disable almost all of the configuration that Cider automatically injects by using the following .dir-locals.el file. The clojure command line with then run just the configuration form the aliases. This is useful to start the REPL and connect to the project using jack-in whilst having full control over the functionality.

Calva jack-in

The Calva jack-in process is similar to Cider although it does not support a .dir-locals.el configuration. Calva does provide a very useful options menu to choose which aliases should be included when it forms the clojure command to run the REPL.

The standard jack-in command created is of the form

Executing task: clojure -Sdeps '{:deps {nrepl {:mvn/version "0.8.2"} cider/cider-nrepl {:mvn/version "0.23.0"} clj-kondo {:mvn/version "2020.04.05"}}}' -m nrepl.cmdline –middleware "[cider.nrepl/cider-middleware]"

/bin/zsh '-c', 'clojure -Sdeps '{:deps {nrepl {:mvn/version "0.8.2"} cider/cider-nrepl {:mvn/version "0.23.0"} clj-kondo {:mvn/version "2020.04.05"}}}' -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]

When including aliases in the jack-in command, Calva will add them before the -m flag in the Clojure command

/bin/zsh '-c', 'clojure -Sdeps '{:deps {nrepl {:mvn/version "0.8.2"} cider/cider-nrepl {:mvn/version "0.23.0"} clj-kondo {:mvn/version "2020.04.05"}}}' -A:env/dev:inspect/portal -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]

Note: Calva will use the -M flag in a future release when including aliases, moving away from the deprecated -A flag in Clojure CLI tools.

User level aliases via a repl connect sequence

Define a repl.connectSequence configuration to use one or more aliases from a user level configuration (e.g. ~/.clojure/deps.edn).

A repl.connectSequence is defined in the VS Code editor settings.json file.

During the jack-in process, select the name of the connection sequence, rather than Clojure CLI, to start the REPL process with just the aliases from the connectSequence. It is not possible to select additional alias names from the project deps.edn.

    "calva.replConnectSequences": [
        {"name": "Inspect Portal",
         "projectType": "Clojure CLI",
         "cljsType": "none",
         "menuSelections": {
            "cljAliases": [
               "env/dev",
               "inspect/portal"]}}],

The repl.connectSequence adds an extra layer of indirection to the jack-in approach and is not as flexible as using connect.

There seems to be an issue using kebab-case alias names with a repl connect sequence

Conjure approach

The approach in Conjure is as simple as opening Neovim with a Clojure project. If a REPL is already running for that project, determined by checking for a port value in the file .nrepl-port, then Conjure will connect automatically.

The Conjure jack-in approach... TODO

Summary

For simple projects and local environments, using jack-in is a quick way to run a REPL.

You should consider using connect if you want a more robust REPL, that can work with local and remote environments, can be accessed by any Clojure editor and can provide more services that just the REPL service (e.g. data visualization tools)

Thank you. @practical_li

Tags: clojure-cli editors cider calva conjure