Skip to content

asampal/node-clojurescript

 
 

Repository files navigation

Build Status

node-clojurescript

node-clojurescript aims to provide seamless integration between NodeJS and ClojureScript. This is a young project, started in May 2012, it's under active development and welcomes participation by the NodeJS and Clojure communities.

Packages are available on the npm registry:  clojure-script.

Description

The ClojureScript library ships with some basic mechanisms for creating compiled scripts suitable for use with NodeJS. But more is possible, and this library aims to make compiling, loading and running .cljs scripts a breeze, in the same manner that CoffeeScript .coffee files can be used transparently in the development of NodeJS-backed applications.

Quick Start

Want to get started? There are some prerequisites, but if you'd prefer to trouble with those later:

$ npm install -g clojure-script

If you get an error message from npm, it means you need to review the Prerequisites section of this README.
Let's continue...

$ touch hello.cljs && vim hello.cljs

Now paste in something like:

(ns hello
  (:require [cljs.nodejs :as nodejs]))

(defn greet [n]
  (println (str "Hello, " n)))
  
(nodejs/next-tick
  (fn []
    (greet "World!")))

Save it, and leave the editor open. In another terminal, navigate to the directory where you created hello.cljs and do:

$ ncljsc hello.cljs

When you invoke ncljsc, it fires up the JVM, the ClojureScript compiler and the Google Closure Compiler. This means the compilation will seem slow, even really slow (10+ seconds), especially if you're used to the sub-second compile times of CoffeeScript. That's expected, and the issue will be revisited in the Faster, faster! section below.

N.B. the compiled JavaScript will be evaluated by the NodeJS (V8) runtime, not by the JVM.

You should eventually see printed in your terminal:

$ ncljsc hello.cljs
Hello, World!

Now replace the contents of hello.cljs with:

(ns hello
  (:require [cljs.nodejs :as nodejs]
            [clojure.string :as string]))

(def http
  (nodejs/require "http"))

(def server (.createServer http
   (fn [req, res]
     (.writeHead res 200 (clj->js {:Content-Type "text/plain"}))
     (.end res "Hello, World\n"))))

(.listen server 4200 "127.0.0.1")

(println "Server running at http://127.0.0.1:4200")

;; Helper
(defn clj->js
  "Recursively transforms ClojureScript maps into Javascript objects,
   other ClojureScript colls into JavaScript arrays, and ClojureScript
   keywords into JavaScript strings. Credit:
   http://mmcgrana.github.com/2011/09/clojurescript-nodejs.html"
  [x]
  (cond
    (string? x) x
    (keyword? x) (name x)
    (map? x) (.-strobj (reduce (fn [m [k v]]
               (assoc m (clj->js k) (clj->js v))) {} x))
    (coll? x) (apply array (map clj->js x))
    :else x))

Save it, then rerun ncljsc hello.cljs and point your browser to localhost:4200.

Did it work? Cool!  (maybe submit an issue if it didn't)

Faster, faster!

The slow compile times mentioned above are owing to startup time of the JVM, plus the time to initially load the two underlying compilers (ClojureScript and Google Closure). This is an annoying problem...

Problem solved!  Starting with v0.1.4, node-clojurescript offers a way to compile against a long-running, "detached" JVM server:

$ ncljsc --server 4242

Invoke the command above and leave the terminal open (or run it in a tmux or screen session). You don't need to navigate to a particular path before starting it, bu you need to leave it running. After 10+ seconds you should see:

$ ncljsc --server 4242
Starting up, please wait...

Initial build completed, JVM and compiler are primed and ready!
Detached JVM server listening at http://127.0.0.1:4242/

If you see something about a DTraceProviderBindings error, just ignore it as it's harmless. Depending on your platform, no error may be reported.

Now open another terminal and go back to the directory where you created hello.cljs. Then do:

$ ncljsc --client 4242 hello.cljs

You should notice a marked difference in the time required for the script to run. Once the --server JVM is "hot", compile times should take only a few seconds, instead of 10+ seconds. That's because the --client process does not start its own JVM.

How does it work?

The --server process accepts "build requests" over HTTP, listening on localhost at the specified port. The --client then makes synchronous or asynchronous requests (depending on how it's invoked). And that's it: from the perspective of the end-user, the only difference is that these "remote" builds happen more quickly than "local" builds. Overall usage of ncljsc is the same whether you run remote or local builds.

A few notes:

  • This feature is under active development and won't always work correctly, e.g. errors may not always make it back to the client and the return value of a build may be an empty string. It's best to keep an eye on the terminal output of the server process for signs of trouble.
  • You may use whatever port number you prefer, as long as the client and server use the same port.
  • Requests are restricted to the localhost interface. (security)
  • There is a transparent exchange of credentials hard-wired into the client-server logic, so that arbitrary processes can't make build requests. (security)
  • Aforesaid "credentials exchange" requires server and client processes to be run as the same user. (security)
  • Credentials aren't persistent, so if the server process is bounced (you restart it, maybe it crashed), client processes must be restarted if they're long-running and will attempt further build requests. (security?)
  • Don't use the client-server mode in a production environment. (goes without saying?)

Automatic re-compiles

It's 2012 and you shouldn't have to manually re-run your scripts while you're developing them. And you don't!

After some experimentation, supervisor seems (to the author) to be the simplest and most flexible NodeJS-based tool for automatically re-starting scripts in a development workflow. Make sure to install it globally: npm install -g supervisor.

With supervisor installed and a ncljsc --server process running, revisit the directory where you created hello.cljs and do:

$ supervisor -w hello.cljs -n exit -x ncljsc -- --client 4242 hello.cljs 

That's a lot of flags for a single command, but see supervisor --help and you'll soon have the hang of it. Note that we're making use of --client 4242, which is proper to ncljsc, not supervisor.

Now edit hello.cljs and watch what happens when you save it. Fantastic!   It compiles quickly, and will do so repeatedly whenever you save changes, so long as you keep supervisor running.

Compiling to disk

In addition to running .cljs scripts, ncljsc can also be used to write compiled JavaScript to disk. For example:

saves to hello.js in the same directory (local build)

$ ncljsc --compile hello.cljs

saves to myscript/hello.js in the same directory (remote build)

$ ncljsc --compile --output myscript --client 4242 hello.cljs

re-compiles and re-saves when changes are made (remote build)

$ supervisor -w hello.cljs -n exit -x ncljsc -- --client 4242 --compile hello.cljs 

More options

The ncljsc command provides additional capabilities. Try:

$ ncljsc --help

Not all of the features have been implemented yet. Also, you'll notice that ncljsc provides built-in --watch and --watch-deps options. Those do work, but there are some outstanding bugs related to NodeJS's fs.watch facility. As such, it seemed better to propose supervisor as a file watching tool than explain a bunch of caveats regarding the built-in watch support. But by all means experiment and report back.

What's Next?

So now what you should do is read up on Clojure and ClojureScript and get to busy!  See the Resources section below.

Prerequisites

Java

This library wraps a NodeJS front-end around the ClojureScript compiler, which is written in the Clojure language, which is hosted on the Java Virtual Machine (JVM). That means you must have Java setup to successfully install clojure-script with npm.

More specifically, you'll need JDK 1.6 (Java SE 6):  Windows,  Mac,  debian / ubuntu

You'll also need to export the proper value for JAVA_HOME into your environment. The installation instructions for the node-java package are quite helpful in this regard, though note that installing clojure-script with npm will automatically install node-java as well (i.e. you don't need to do that separately).

NodeJS

If you're new to NodeJS and don't have it setup, that will be your next step. I highly recommend Tim Caswell's Node Version Manager (nvm). It's easy to install and, and makes working with multiple node versions dead simple. For example:

$ nvm install v0.6.17
...
$ nvm install v0.6.10
...
$ nvm use v0.6.17

When you install node, the npm tool will get installed along with it. So as long you have Java in place (see above), you should be ready to run:

$ npm install -g clojure-script

That's it! Installing clojure-script (the npm package name for this library) will automatically perform a package-localized installation of Clojure, ClojureScript, Google Closure Compiler, etc.

If you get an error during installation, look closely at the error message. Maybe you made a typo while following the steps above? If you can't figure it out, feel free to submit an issue.

NodeJS require support

With a local (vs. global) node_modules installation of clojure-script, you can load .cljs modules from other scripts without having to compile them beforehand.

With respect to the Quick Start examples above: create a script other.js in the same directory as hello.cljs, then paste in the following and save it:

require('clojure-script');

require('./hello');
// or require('./hello.cljs');

Then you can do:

$ node other.js

N.B. This is a handy feature for development purposes. But it would be a terrible idea to publish a package on the npm registry which makes use of this technique. Modules developed in ClojureScript should be published with only compiled JavaScript loaded at runtime (via NodeJS's require).

Remote builds and require

It's entirely possible to leverage the require support in combination with node-clojurescript's client-server mode described in the Faster, faster! section above.

Suppose you have a "detached" JVM server process running on port 8888:

$ ncljsc --server 8888

In your .js script you can then indicate:

require('clojure-script')(8888);

require('./hello.cljs');

In this context, the client-logic makes a synchronous (not async) build request against the server process. hello.cljs will be transparently compiled and loaded as before, but more quickly.

You can call the function returned by require without arguments, like so:

require('clojure-script')();
...

In that case, the port number will default to 4242 (make sure the server process is using the same port). Note that calling the function without arguments and not calling it are two distinct things. If you don't call it, the clojure-script module will start a new JVM and perform a local build. If you do call it, with or without arguments, a JVM will not be started and the module will make a remote build request.

Namespaces

Clojure and ClojureScript support the notion of namespaces. Unlike loading modules with NodeJS's require, using ClojureScript's namespace :require will result in the namespaced module being inlined as part of the compiled JavaScript (scope is carefully preserved).

Try creating two scripts, foo.cljs and bar.cljs:

foo.cljs

(ns foo
  (:require [cljs.nodejs :as nodejs]
            [bar :as bar]))

(defn ^:export greet [name, title]
  (str "Hello, " (bar/title title) " " name))

bar.cljs

(ns bar
  (:require [cljs.nodejs :as nodejs]))

(defn ^:export title [t]
  (str t " Amazing" ))

Now create a third script, greet.js:

greet.js

var foo = require('./compiled.js').foo;

console.log(foo.greet('ClojureScript developer!', 'Mr.'));

Time to compile:

$ ncljsc -c -p foo.cljs > compiled.js

When that's finished, it's time to run greet.js:

$ node greet.js
Hello, Mr. Amazing ClojureScript developer!

Examining the plentiful contents of compiled.js, you'll see (toward the bottom) that both foo.cljs and bar.cljs were compiled into the stand-alone JS file.

Bundled Leiningen

Leiningen is a popular and flexible build tool in wide use among Clojure developers. node-clojurescript bundles the shell script front-end to Leiningen (the lein command) and proxies to it with an executable script named nlein.

If you've installed the clojure-script module globally with npm install -g clojure-script, then you should be able to run:

$ nlein

nlein is a simple proxy script and does not feature any customizations of Leiningen. If you already have a lein executable on your path, nlein will ask whether it should delegate to it, with the option to remember your decision.

Note that nlein, when it's not delegating to another lein, will store JAR files and other things it downloads in the support/.lein directory, relative to the root of the clojure-script package. This is to keep nlein from even potentially conflicting with an existing installation. Normally, lein stores such things in $HOME/.lein.

All in all, the purpose of nlein is to provide an easy way for NodeJS developers to get up and running with Leiningen. If you're already using Leiningen, you may choose to ignore nlein and go about your business as usual.

Coming Soon

There are several goals that need to be accomplished in short order:

  • The tooling developed in CoffeeScript needs to be re-implemented in ClojureScript so that this library will be pseudo self-hosting.
  • A plugin for the Leiningen build tool needs to be adapted or written, for use in development of complex ClojureScript projects in conjunction with this library and other NodeJS modules.
  • Missing features of ncljsc need to be implemented, the most important being a REPL.
  • More and better documentation and examples.

Help in accomplishing these and future goals is more than welcome.

Resources

$ ncljsc --help

ClojureScript: Translations from JavaScript

ClojureDocs,  Clojure.org

NodeJS Documentation

Leiningen, wiki

Google Groups: clojure,  nodejs

#clojure, #clojurescript and #node.js channels on Freenode IRC.

prelude,  prelude-modules,  and Emacs for OS X (latest pretest)

Credit

This software is derived from and incorporates existing works:

CoffeeScriptClojureScriptPomegranate

In particular, many thanks are owed to Jeremy Ashkenas and the other CoffeeScript maintainers. Using the CoffeeScript tooling as a template, it was possible to whip together a useable NodeJS front-end in one intense week. It would have otherwise been much more slow-going.

Copyright and License

This software is Copyright (c) 2012 by Michael Bradley, Jr.

The use and distribution terms for this software are covered by the Eclipse Public License 1.0 which can be found in the file epl-v10.html under the licenses directory at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.


JavaScript Reference

About

seamless integration between NodeJS and ClojureScript

Resources

Stars

Watchers

Forks

Packages

No packages published