How to author agnostic JavaScript library in ClojureScript?
Asked Answered
R

2

5

Let's say I have a cljs file containing the following:

(ns foo)
(defn add [x y]
  (+ x y))

and wish to make this available as a JavaScript library to non-ClojureScript devs (primarily focused on node.js). I can do this:

clj -m cljs.main -c foo

But the problem is that the output is geared towards google closure's module system (e.g. goog.require). I can set the target to none with the -t flag (as opposed to browser or node), and that... doesn't fix this. Setting it to node also doesn't fix the issue: no index.js (it's called main like in Java), no module.exports = blah blah. Seems like it's geared towards standalone, full node apps, rather than libraries.

I understand that ClojureScript uses google closure for it's own sub-modules, and I'm not necessarily looking to get rid of all of that (I'm not sure you could). And I get that es2015 native JavaScript modules are out because of their static nature.

I could massage the output by hand or by script to play nice with the npm ecosystem, but I'm surprised that there's no compiler option that can actually output a npm-friendly module. Or is there? Am I just reading --help wrong?

Ringer answered 19/10, 2018 at 14:28 Comment(0)
P
4

shadow-cljs supports output in a CommonJS format via :target :npm-module which does support exactly what you are asking for. Node and other JS tools (eg. webpack) can consume separate namespaces independently. The default CLJS tools do not support this mode.

ClojureScript however is very much written with the assumption that your whole program will be optimized by the Closure Compiler. This makes it less than ideal for writing libraries to be included in other builds. Each "library" built this way will contain its own version of cljs.core and therefore will be pretty large to begin and including 2 libraries built this way is a recipe for disaster since they won't be compatible with each other.

Putumayo answered 19/10, 2018 at 19:59 Comment(3)
I'm not concerned as much about the size (although all things being equal of course smaller is better). I was hoping to use clojurescript as a nicer language for writing atom editor plugins, but it sounds like there's too much friction. – Ringer
Very low friction if code size is not a main priority. It is not a problem unless you intend to write many different plugins that directly interact with each other. – Putumayo
I think I'll just have to give it a go and see how it works out. – Ringer
U
5

This assumes that you already have a working installation of ClojureScript and Node.js


Given:

math101
|-- package.json
|-- src
|   `-- com
|       `-- example
|           `-- math.cljs

package.json

{
  "name": "math101",
  "version": "1.0.0",
  "main": "dist/index.js",
  "license": "MIT",
  "devDependencies": {
    "shadow-cljs": "^2.8.52",
    "source-map-support": "^0.5.13"
  }
}

Notes:

  • dist/index.js - This will contain our ClojureScript code converted to JavaScript
  • shadow-cljs - The build (and dependency management) tool that we need
  • source-map-support - Required to run ClojureScript on Node.js

πŸ“£Please make sure you have installed these two NPM dependencies before you proceed further.

math.cljs

(ns com.example.math)

(defn add [x y]
  (+ x y))

1. Setup the build tool

At the root of math101 run yarn shadow-cljs init.
This will create a file called shadow-cljs.edn with some default settings:

;; shadow-cljs configuration
{:source-paths
 ["src/dev"
  "src/main"
  "src/test"]

 :dependencies
 []

 :builds
 {}}

Let's make some changes.

First you don't need that many source paths for this:

{:source-paths
 ["src"]

 :dependencies
 []

 :builds
 {}}

Then let's add a build configuration:

;; shadow-cljs configuration
{:source-paths
 ["src"]

 :dependencies
 []

 :builds
 {:math101 {:target :node-library
            :output-to "dist/index.js"
            :exports-var com.example.math/add}}}

Notes:

  • :math101 - This the build id that we will use later
  • :target :node-library - This tells shadow-cljs that you intend to author a library
  • :output-to "dist/index.js" - The code you intend to publish
  • :exports-var com.example.math/add - "Fully qualified name" of the function you intend to publish. This will produce a default export.

Additional notes:

The :target :node-library emits code that can be used (via require) as a standard node library, and is useful for publishing your code for re-use as a compiled Javascript artifact.

Source

There is a :npm-module target available but so far :node-library has checked all the boxes for me.

2. Let's build this!

Run yarn shadow-cljs compile math101.
The first time you run this, shadow-cljs will download a bunch of stuff. Eventually it will finish and when it does...

$ node
> var add = require('./dist')
> add(40, 2)
42

✨✨✨

Need to export more than just one function? No problemo.

Let's add subtract:

(ns com.example.math)

(defn add [x y]
  (+ x y))

(defn subtract [x y]
  (- x y))

And now let's update our build config:

;; shadow-cljs configuration
{:source-paths
 ["src"]

 :dependencies
 []

 :builds
 {:math101 {:target :node-library
            :output-to "dist/index.js"
            :exports {:add com.example.math/add
                      :subtract com.example.math/subtract}}}}

Run yarn shadow-cljs compile math101 again and when it finishes:

$ node
> var math101 = require('./dist')
> math101.add(40, 2)
42
> math101.subtract(44, 2)
42

✨✨✨✨✨✨


ADDENDUM

This is only intended to get you started. shadow-cljs compile generates non-production code (i.e. it's not minified, dead code is not removed AFAIK and Google Closure hasn't run any optimisation yet).

The command for generating production-ready code is shadow-cljs release but you may want to tweak your build config before.

I would highly recommend that you invest time reading up the shadow-cljs documentation. It is an amazing tool.

Unreflective answered 17/9, 2019 at 20:22 Comment(1)
Thank you so much for this detailed explanation, you helped me alot. – Stroganoff
P
4

shadow-cljs supports output in a CommonJS format via :target :npm-module which does support exactly what you are asking for. Node and other JS tools (eg. webpack) can consume separate namespaces independently. The default CLJS tools do not support this mode.

ClojureScript however is very much written with the assumption that your whole program will be optimized by the Closure Compiler. This makes it less than ideal for writing libraries to be included in other builds. Each "library" built this way will contain its own version of cljs.core and therefore will be pretty large to begin and including 2 libraries built this way is a recipe for disaster since they won't be compatible with each other.

Putumayo answered 19/10, 2018 at 19:59 Comment(3)
I'm not concerned as much about the size (although all things being equal of course smaller is better). I was hoping to use clojurescript as a nicer language for writing atom editor plugins, but it sounds like there's too much friction. – Ringer
Very low friction if code size is not a main priority. It is not a problem unless you intend to write many different plugins that directly interact with each other. – Putumayo
I think I'll just have to give it a go and see how it works out. – Ringer

© 2022 - 2024 β€” McMap. All rights reserved.