How to deal with development code in ClojureScript?

In React community, there are some cases we write:

if (__DEV__) {
  console.log("some warning or asserting")
}

and these lines of code will be removed in Webpack release mode.

Can we do this in ClojureScript?

I thought about using macros with a variable to decide while function to use. But perhaps we still need the compiler to do something about it?

There are a couple things people do. One thing is to check for goog.DEBUG, which is what the Google Closure library also uses. In your ClojureScript compiler options you can add

:closure-defines {goog.DEBUG false}

for the production build, and {goog.DEBUG true} for the dev build, and then check for it in your code, (if goog.DEBUG ...)

This doesn’t stop code from being included in the final build though. You can come up with a macro scheme, although I don’t immediately have a good example. You would have to rely on something else than goog.DEBUG since macros are evaluated in the JVM (when using the default Clojure based compiler).

A more common approach that people use is to make use of separate dev/prod source paths.

[{:id "dev"
  :source-paths ["src" "env/dev"]
  :compiler { ,,, }}

 {:id "prod"
  :source-paths ["src" "env/prod"]
  :compiler { ,,, }}]

and then having certain namespaces in there with different function or macro definitions based on dev or prod. You could even have a generic when-dev macro for instance

;; env/dev/my/macros.clj
(ns my.macros)

(defmacro with-debug [& body]
  body)
;; env/prod/my/macros.clj
(ns my.macros)

(defmacro with-debug [& body]
  nil)

So any code you wrap in (with-debug ,,,) will be removed from the output when doing a production build.

3 Likes

Closure will definitely remove conditional code if the conditional can be resolved at compile time. It sometimes takes a type hint to remove the cljs.core.truth_ check which CLJS injects to have proper truth semantics.

Take this code:

(when js/goog.DEBUG
  (js/console.log "hello world"))

Usually this will emit

if(cljs.core.truth_(goog.DEBUG)){
console.log("hello world");
} else {
}

Note the cljs.core.truth_ call. This is not recognized by Closure and will not be removed. However with a little boolean hint this can be removed.

(when ^boolean js/goog.DEBUG
  (js/console.log "hello world"))

turns into

if(goog.DEBUG){
console.log("hello world");
} else {
}

The Closure Compiler does properly recognize this and once you run this through :advanced (or even :simple) this if will the completely removed.

Macros can also work but I generally advise against them since build tools will not be aware of any trickery you do with environment variables and such. Caching will usually become very unreliable if you do this and will often mean that lein clean or equivalent is your only option for reliable build output. Not something I would want to trade.

Another option would be reader conditionals but CLJS currently does not support this. It would be pretty easy to add though.

PS: It is unfortunate that the ^boolean type hint is still required in all cases, we can definitely get rid of some. https://dev.clojure.org/jira/browse/CLJS-1439 has been open quite a while, maybe some votes would help. :wink:

4 Likes

That is super interesting and explains why e.g. the re-frame template has this

(def debug?
  ^boolean goog.DEBUG)

I’ve upvoted the ticket!