Implementing JavaScript interop for a compile-to-JS language

Answered

I develop Cursive, a plugin for Clojure development. I'd like to implement better support ClojureScript in Cursive, which is the dialect of Clojure which compiles to JavaScript. I've looked around but there's very little information on how to provide support for compile-to-JS languages, and most of the current implementations seem to assume a fairly similar syntax to JS. ClojureScript's is very different, and I can't extend my PSI elements from JSExpression, JSStatement and friends.

Here are some examples of things I need to support:

The js prefix allows access to the global object.

(js/parseInt "222")

Object constructor calls.

(js/RegExp. "^foo$")

Instance method invocations.

(def re (js/RegExp. "^Clojure"))
(.test re "ClojureScript")

Static method invocations.

(.sqrt js/Math 2)
;; or
(js/Math.sqrt 2)

Object property access

(.-multiline re)
(.-PI js/Math)

Simple JS object creation:

(js-obj "country" "FR")
;; or
(def myobj #js {:country "FR"})

(similar to)

var myobj = {country: "FR"};

And then...

(.-country myobj)

Construction of JS objects from Clojure data:

(clj->js {:foo {:bar "baz"}})
(into-array ["France" "Korea" "Peru"])

ClojureScript compiles down to Google Closure compatible JavaScript, and then runs it through the Closure compiler afterwards. So Google Closure interop is very important:

(ns yourapp.core
(:require [goog.dom :as dom]))

(def element (dom/getElement "body"))
(ns yourapp.core
(:import goog.History))

(def instance (History.))

There is also syntax for integrating Node modules (here :refer imports a symbol directly):

(ns example.core
(:require [react :refer [createElement]]
["react-dom/server" :as ReactDOMServer :refer [renderToString]]))

(js/console.log (renderToString (createElement "div" nil "Hello World!")))

I'd love some guidance on how to implement symbol resolution and code completion for the above use cases, as well as how to provide the JS integration with information about the JS objects created in CLJS code. Can I make calls like "what are the elements available on the global object/an object of this type/some Closure namespace/some node module" and "what are all the available Node modules for this file"?

I'd also really love to be able to leverage the type inference and dataflow features. From what I've seen, they require my PSI to extend JSStatement/JSExpression and so on - is that the case? If so, can I get information about types from the existing JS indexes to provide some of that functionality myself? Are the types compatible between native JavaScript, Closure and Typescript, or do I have to convert them somehow? In ClojureScript, the native types (string, number, regex, nil) etc compile to their JavaScript equivalents.

I know this is a question with probably a massive answer, so many thanks for any and all guidance.

3 comments
Comment actions Permalink

Colin,

It is indeed a non-trival question. First of all, I think that you might need to either replace the Clojure AST with JS-compatible one, or you can try using MultiplePsiFilesPerDocumentFileViewProvider with having a secondary AST JS-compatible. I think you should be able to create an AST for Clojure based on JSElement. I would suggest getting a proof of concept, where you can resolve reference from ClojureScript to JS/TS and vice-versa and than experiment with multiple PsiFiles. Once you have JS-compatible model available you can look for some answers in our open-source plugins - Angular and Vue plugins, which implement their own JS-like expression support (e.g. custom resolution and code completion). I'll be glad to assist you!

0
Comment actions Permalink

Hi Piotr,

There are various complications with creating a JS-compatible AST. In general, when parsing Clojure code I only create a very simple AST. This is because Clojure, as a lisp, is very macro-focused. Even the majority of the built-in features (defn for defining functions, for example) are based on macros. This means that I need symbol resolution in order to build the AST, and I can't do it during parsing. An additional complication is that users can define their own macros so this needs to be extensible - see https://cursive-ide.com/userguide/macros.html#customising-symbol-resolution for how this works from the user's perspective.

So what I do is only create a very simple AST during parsing which basically just contains the data structures in the source (symbols, keywords, strings, lists, vectors, maps etc) and then I lazily parse the forms when required for editor functionality.

The other complication is CLJC, which is similar to a template language for sharing code between Clojure and ClojureScript. You can get an idea of what this looks like here: https://danielcompton.net/2015/06/10/clojure-reader-conditionals-by-example . My support for CLJC is actually very messy at the moment, and I wonder if MultiplePsiFilesPerDocumentFileViewProvider might provide a cleaner solution (i.e. I could create a Clojure and ClojureScript PSI from a CLJC file).

So I think that creating a JS-compatible AST is probably pretty tricky. Is it possible to get much functionality without this?

0
Comment actions Permalink

Colin,

I've looked a little bit at the syntax and I think you should be able to build JS-compatible syntax tree, which would work with our resolution engine. However, there are a lot of "ifs" and I am not really into Clojure myself, so I think it would be better to schedule a call to discuss possible solutions. Please ping me on my corporate e-mail - piotr dot tomiak at jetbrains dot com.

0

Please sign in to leave a comment.